Champion Portal Roadmap
Track the development progress of the Belmont Alumni portal. This page shows what's working and what's still in development.
Phase 1: Foundations ✓ Complete
Auth, Profile, Directory, Regional structure, Dashboard — the core identity and discovery features.
View DocumentationDatabase tables and models for Champion Portal foundations.
Features:
- Cp::Champion model with Devise integration
- Cp::ProfileChange model for CRM export tracking
- Districts and Regions with associations
- ZIP code lookup (39,305 ZIP codes → 820 districts → 7 regions)
- Geographic seed data rake task
Email-based signup, verification, login, and password reset.
Features:
- Signup with name, email, ZIP code
- Email confirmation with password setting
- Login/logout with session management
- Password reset flow
- Custom Devise mailer for champion subdomain
- Mobile-first responsive UI
Social login with Google OAuth.
Features:
- Google OAuth integration via OmniAuth
- SSO callback controller
- ZIP code prompt after SSO signup
- Profile controller for view/edit
- Multi-model Devise OmniAuth support
Profile completion wizard and Champion directory search.
Features:
- 5-step profile wizard (linear onboarding flow)
- Section-based profile editor (tabbed for returning users)
- Profile show page ('How Others See You' preview)
- Photo upload with ActiveStorage
- Affinities multi-select picker
- Privacy indicators on profile
- Education section from linked alumni record
- OAuth BUID conflict resolution
Admin verification tools in Lookup Portal for managing champion BUID linking.
Features:
- Champions namespace at /champions with portal_admin access
- Dashboard with quick stats, verification funnel, regional breakdown
- Verification Queue: approve/reject pending BUID matches
- AJAX alumni search by name, email, or BUID
- Manual BUID linking for champions without matches
- District Management in Settings > Reference Data
- 41 new tests (18 verifications + 23 districts)
Complete Staging MVP: champion dashboard, directory enhancements, activity tracking.
Features:
- Activity tracking infrastructure (cp_activity_events table)
- Cp::ActivityRecorder service for login, profile_edit, directory_search, profile_view
- 'Alumni in Your District' preview cards
- Community stats (total alumni, alumni in district)
- District-only location visibility for dashboard and directory
- Graduation Year filter (hierarchical: decades + individual years)
- College filter (from linked alumni records)
- District filter (autocomplete, reuses existing district_autocomplete_controller.js)
- Affinity typeahead UI (backend exists)
- Sort dropdown (Name A-Z, Recently Joined)
Champion-to-Champion direct messaging with smart email notifications.
Features:
- Threaded conversations (1:1 with group-ready schema)
- Message inbox with unread badges and conversation previews
- 'Send Message' button on directory profile cards
- Emoji reactions (❤️ 😂 😮 😢 😠 👍) with optimistic UI
- Smart notifications (email if unread after 5 min, background job)
- Mute and archive thread controls
- Activity tracking (message_thread_started, message_sent)
- Only verified Champions can send/receive
Champions admin dashboard with engagement scoring for staff visibility into Champion activity.
Features:
- Champions Admin Dashboard (/champions namespace)
- Searchable/filterable Champions list (name, email, verification, role, district)
- Pagination (25 per page) with sort options (signup date, last name)
- Champion detail view with activity timeline and stats
- Profile completion % calculation (10 core fields) with progress bars
- Engagement Score Service (reusable activity-based scoring)
- Engagement scoring: 5pts (login/conversation/invite), 3pts (edit/message), 1pt (view/reaction), 0.5pt (search)
- Engagement levels display (Highly Engaged/Engaged/Active/New)
- Impersonation feature ('Log in as Champion' for testing/support)
- Environment-aware routing (admin portal ↔ champion portal)
- Color-coded badges (completion %, verification status, engagement level)
- Comprehensive engagement scoring documentation with extensibility pattern
- 19 admin dashboard tests + 8 engagement score service tests
Settings, Help/FAQ, visual refresh, and language audit before beta launch.
Features:
- ✅ 1.9.1: Champion Settings page (account, connected accounts, notifications)
- ✅ 1.9.1: Email address change flow with verification
- ✅ 1.9.1: Google account connect/disconnect management
- ✅ 1.9.2: Help & FAQ page with accordion UI and real-time search
- ✅ 1.9.2: Onboarding tooltips for new Champions (4 initial tooltips)
- ✅ 1.9.3: Profile visual refresh (simplified cards, college icons, education format)
- ✅ 1.9.3: Directory card refresh (accent bar, photo focus, degree display)
- ✅ 1.9.4: Dashboard visual refresh (warmth, personalization, community feel)
- ✅ 1.9.5: Language Style Guide based on Alumni Champions Verbal Style Guide
- ✅ 1.9.5: Fixed 'prospective students' → warm alumni language in mailers
- ✅ 1.9.5: Updated empty states and flash messages for warmth
Admin-managed news/posts system with dashboard integration and regional targeting.
Features:
- ✅ 1.10.1: Database schema (posts, images, regions, views, likes)
- ✅ 1.10.2: Staff admin CRUD (Lookup Portal, global posts)
- ✅ 1.10.3: Champion-facing news views (dashboard cards, news index, detail pages)
- ⏸️ 1.10.4: CLC regional admin (deferred — CLC model evolving)
- ⏸️ 1.10.5: Engagement features (likes — deferred to Phase 2+)
- ⏸️ 1.10.6: Polish (image carousels, featured champions — deferred)
Staff-managed events system with district targeting and Champion Portal integration.
Features:
- ✅ 1.11.1: Database schema (cp_events + cp_event_districts for district targeting)
- ✅ 1.11.2: Staff admin CRUD (Lookup Portal, event management with status workflow)
- ✅ 1.11.3: Champion-facing events views (events index with filters, detail pages)
- ✅ 1.11.4: District-filtered event display (mirrors News pattern)
- ✅ 1.11.5: View counting and RSVP click tracking with activity events
- ✅ 1.11.6: Navigation integration (Events link in header)
- ⏸️ Dashboard widget (deferred to 1.12 layout update)
- Note: External RSVP links only (no built-in RSVP in MVP)
Database schema, models, and basic UI for emergent communities.
Features:
- ✅ 1.12.1: Database schema (cp_communities, cp_champion_communities, cp_community_suggestions)
- ✅ 1.12.2: Cp::Community model with 6 community types
- ✅ 1.12.3: Suggestion model and CommunityMatchingService
- ✅ 1.12.4: Profile update hooks (auto-suggest on profile changes)
- ✅ 1.12.5: News/Events community targeting
- ✅ 1.12.6: Admin Communities UI (Lookup Portal)
- ✅ 1.12.7: Champion Community UI (My Communities, join/leave, suggestions)
Champion-facing community browsing and enhanced landing pages.
Features:
- ✅ 1.13.1: Community show page with members, news, events
- ✅ 1.13.2: Communities index (My Communities + Suggestions + Explore All)
- ✅ 1.13.3: Dashboard 'My Communities' widget (replaced Community Snapshot)
- ✅ 1.13.4: Navigation updates (Communities | Directory | Messages | Name)
- ✅ 1.13.5: Testing & polish (1661 tests, 0 failures)
Background detection and creation of emergent communities when 3+ champions share an attribute.
Features:
- ✅ 1.14.1: CommunityDetectionService (threshold checking)
- ✅ 1.14.2: CommunityCreationJob (background processing)
- ✅ 1.14.3: CommunityNotification model + migration
- ✅ 1.14.4: Dashboard notification banner for new communities
- ✅ 1.14.5: Detection triggers (verification, profile wizard, profile edit)
- ✅ Testing: 55 new tests, 1716 total suite tests
Community images, force creation, membership management, message link previews.
Features:
- ✅ 1.15.1: Community Image Upload (hero + thumbnail with drag/drop)
- ✅ 1.15.2: Force Create Communities (admin creation from any attribute)
- ✅ 1.15.3: Expanded Membership Management (join requests, admin member tools)
- ✅ 1.15.4: Message Link Previews & Sharing (community cards, invite flow)
In-app notifications with bell icon, email digests, and user preferences.
Features:
- ✅ 1.16.1: Database & Models (cp_notifications, cp_notification_preferences tables)
- ✅ 1.16.2: NotificationService with smart aggregation for high-volume types
- ✅ 1.16.3: Navbar UI (bell icon dropdown, mark read, clear all)
- ✅ 1.16.4: Settings Integration (per-type preferences in Settings page)
- ✅ 1.16.5: Email Digests (daily/weekly via NotificationDigestJob)
- ✅ 1.16.6: NotificationCleanupJob (removes old read notifications)
Phase 1C: Role Selection & Quiz ✓ Complete
Champion Role in Profile Wizard and Quiz Integration — champion identity features.
View DocumentationExtract quiz/role logic from signup helper into reusable services.
Features:
- ChampionRoleService (role definitions, colors, narratives)
- ChampionQuizService (questions, scoring, results)
- Update ChampionSignupsHelper to delegate to services
- Cp::ChampionRolesHelper for portal
- 42 comprehensive tests (24 role + 18 quiz)
Add Champion Role step to profile wizard with optional quiz.
Features:
- champion_role step in wizard (step 2 of 6)
- Role selection with 4 role cards (emoji, title, description)
- 'Take the Quiz' button for 7-question quiz
- Full quiz flow with progress indicator
- Quiz results page with personalized narrative
- 'Skip for now' option
- 7 new integration tests
Allow champions to change role and retake quiz from profile edit.
Features:
- Champion Role section in profile edit sidebar
- Current role display with colored card
- 'Retake Quiz' and 'Choose different role' options
- Context-aware quiz navigation (returns to correct page)
- Simplified name fields with toggle for legal name
- Clean data architecture (no duplicate name storage)
Phase 2: Community Leadership ✓ Complete
CL role assignment, permission system, and staff action items — enabling Champions to lead communities.
View DocumentationUpdate ClcAssignment to link to Community instead of Region.
Features:
- Migration: add community_id to clc_assignments
- Migration: remove region_id, add unique constraint
- Model associations and validations updated
- Test fixtures updated for new structure
Enable Lookup Portal admins to assign Champions as Community Leaders.
Features:
- CL assignment list (filterable by community type, search)
- Contextual assignment from champion detail or community detail
- Multi-select: assign champion to multiple communities at once
- Multi-select: assign multiple verified members as CLs for a community
- Membership enforcement: CLs must be members of the community
- Remove CL assignments with confirmation
- CL count indicator on community index
- CL badge and 'Assign as CL' links in community member list
- 21 controller tests for full coverage
Granular permission flags for staff access control.
Features:
- Permission flags: can_portal_admin and can_support_respond
- Role badges in user list (Staff/Admin + Portal Admin/Support)
- Permission toggles in user edit page
- Backward compatible with existing portal_admin role
- 19 controller tests for permission system
Bell icon with pending action items for staff.
Features:
- Bell icon in navbar with badge count
- Permission-filtered action items
- Support requests, champion verifications, new signups
- Communities needing leaders, new communities, CRM pending
- Priority count (support + verifications) vs dot for other items
- ActionItemsService with 9 tests
- Engagement score visible in CL assignment page
- CL assignment notification (email + in-app)
Phase 3: Discussion Boards ✓ Complete
Per-community boards and national forum — enabling threaded community discussions.
View DocumentationDatabase structure for boards, posts, and comments.
Features:
- 8 database tables: board_posts, board_comments, board_reactions, post_flags, user_blocks, hidden_contents, moderation_actions
- 7 models with ActionText, enums, validations, scopes
- Read-only boards controller with index/show actions
- National 'Alumni Champions' community (auto-join for verified Champions)
- Discussions section on community show page
- Activity events: board_view, post_view
- 112 new tests (96 model + 16 controller)
Browse boards, create posts, and comment.
Features:
- New post form (title 100 char, body 40K char, optional image)
- Post truncation with 'See more' on listing
- Threaded comment replies (MAX_DEPTH=2)
- Edit/delete within 30-minute window + 'Edited' indicator
- Community Guidelines reminder in composer
- Discussions in community index featured cards
- 9 new FAQ entries for Discussions
- 20 new controller tests for BoardCommentsController
Emoji reactions and popularity sorting.
Features:
- Emoji reactions on posts and comments (6 options)
- Horizontal emoji bar UI (Messages pattern)
- Popularity score with 14-day decay
- Hot Discussions section (max 2) on discussion index
- Popular discussions on dashboard/community views
- Activity tracking for reactions
Champions can protect themselves from unwanted content.
Features:
- Report/Flag content (posts and comments)
- Flag reason selector (spam, inappropriate, harassment, off-topic, other)
- Hide content from my view only
- Block user (hides all their content, prevents messaging)
- Blocked user notifications filtered from dropdown and count
Community Leader and staff moderation tools.
Features:
- CLC: Hide/unhide content
- CLC: Lock/unlock posts (prevent comments)
- CLC: Pin/unpin posts
- CLC: Escalate to Engagement Team
- CLC: Moderation queue with pending flags only
- Staff: Discussions admin interface (/champions/discussions)
- Staff: View escalated, all, hidden, deleted content
- Staff: Resolve escalations
- Staff: Action items dropdown integration (flagged + escalated counts)
- Moderation counts synced across Leadership index, community dashboard, queue
Global discussion space with auto-join for all verified Champions.
Features:
- 'Alumni Champions' national community with national: true flag
- Auto-join all verified Champions on verification
- Cannot leave national community (permanent membership)
- Per-community notification muting in Settings → Notifications
- Hidden from non-verified Champions
- Main nav link and community integration
- Popular national posts surfaced on dashboard
- News/events without community assignment default to National Board
- Community Leaders can pin posts to national board
Discussion-specific notifications and email digests.
Features:
- Notification triggers: replies, reactions, community posts
- In-app notifications in dropdown with unread badge
- Email digest notifications (daily/weekly configurable)
- Discussion-specific notification types: discussion_reply, discussion_reaction, community_post
- User preferences for email frequency (immediate, daily, weekly, off)
- Blocked user notifications filtered from counts/display
- Email digest templates with activity summary
- Heroku Scheduler jobs for digest generation
Staff dashboard for monitoring discussion board activity.
Features:
- Lookup Portal → Insights → Discussion Boards tab
- Key metrics: posts, comments, active discussers, pending flags
- Trends: Posts/comments per week with sparkline charts
- Community breakdown: Top 5 communities by activity
- Moderation stats: Flags by status (pending/resolved/dismissed)
- Period selector: This Week/Month/Quarter/All Time
- EngagementStats::DiscussionBoardsService for metrics calculation
- Pending flags count links to moderation queue
- 45+ tests for service and controller
Public preview pages for shareable links and signup conversion.
Features:
- Public event pages for global and public community events
- Public discussion post pages for public community posts
- Public community landing pages with activity metrics
- Reusable _public_cta_box.html.erb component
- Return path storage for post-signup/login redirect
- MetaTagsHelper for rich Open Graph/Twitter Card previews
- public_display_name method (FirstName L. format)
- Help page publicly accessible (moved to footer)
Final v2 polish for confirmation UX and staff review detail surfaces.
Features:
- News Index: Featured + compact card layout
- Staff show: v2 field visibility (college/major/profession/affinity details)
- Events: Past Events page with Load More pagination
- Discussions Index: Cross-community /discussions route
- Compact partials: _news_card_compact, _event_card_compact, _discussion_card_compact
- Event row partial with date badge styling
Redesign dashboard with Google News-style content sections.
Features:
- Discussions section: 1 featured + 3 compact cards
- News section: 1 featured + 3 compact cards
- Events section: Date badge rows with community pills
- Communities card: CL indicator (⭐) + Leadership Dashboard button
- Community show page: Restructured with Google News layouts
- Community index: Redesigned as 'Community Select' page
- Layout: Background opacity, footer visibility, consistent max-w-7xl widths
Phase 4: Mobile-First Interface Cleanup ✓ Complete
Polish pass for mobile consistency, design alignment, and UX improvements before MVP launch.
View DocumentationReview and fix all views for mobile-first compliance.
Features:
- Bottom nav overlap fixed globally (pb-20 sm:pb-0)
- Profile wizard button visibility fixed
- Touch targets verified (44px minimum)
- Thumb-friendly navigation verified
- No horizontal scrolling on content
Apply DESIGN-GUIDELINES.md patterns consistently.
Features:
- Consistent button styles across views (w-full sm:w-auto)
- Uniform card margins standardized
- Back link partial created and standardized
- Consistent pill navigation for profile/settings
- Collapsible directory filters on mobile
Final polish including image variant sizing.
Features:
- Event list card redesign with 8:5 aspect ratio
- Image variant sizing audit (7 files fixed)
- Dashboard mobile column reordering
- Hero section heights reduced
- Long text truncation fixed
- scroll_to_selected_controller.js for auto-scroll
Phase 5: Pre-MVP Updates ✓ Complete
Final features for MVP launch — user experience polish, non-alumni support, friending, photo albums.
View DocumentationPolish user experience for different verification states and anonymous visitors.
Features:
- Anonymous landing page at belmontalum.com
- Champion/city count social proof
- Navigation access controls (browse vs act strategy)
- Community suggestions protected for verified only
- Community show page eligibility messaging overhaul
Almost Alumni and Faculty/Staff can become Champions with appropriate fields and verification workflows.
Features:
- ✅ Membership type enum (alumni, almost_alumni, faculty_staff)
- ✅ Almost Alumni fields (anticipated_program, anticipated_college_code, anticipated_graduation_date)
- ✅ Faculty/Staff fields (work_email, profession, affiliated_college_code, affiliated_program)
- ✅ Signup wizard education step with membership-type-specific fields
- ✅ Verification queue support for Almost Alumni and Faculty/Staff
- ✅ Directory filters by membership type (Alumni default, filters for others)
- ✅ Profile display: badges, anticipated education for Almost Alumni
- ✅ Profile display: profession/affiliation for Faculty/Staff
- ✅ Directory cards show anticipated education for Almost Alumni
Save Champions as contacts for quick access and prioritized content.
Features:
- ✅ Add/remove contacts from profile, directory, messages
- ✅ Contact button with icon states (filled/outline user icon)
- ✅ Mutual contact indicator (amber highlight when both add each other)
- ✅ Contacts page (/contacts) with search and grid layout
- ✅ Email + in-app notification when added as contact
- ✅ Privacy enforcement: contact visibility respects email_privacy settings
- ✅ Counter cache for unread messages/notifications (badge performance)
- ✅ Login drift correction for unread counters
- ✅ Message privacy precomputation in controller (query optimization)
Suggest similar Champions based on shared attributes.
Features:
- ✅ AlumniLikeMeService with multi-factor scoring algorithm
- ✅ Scoring: College (+20), Major (+25), Grad Year (+15), Industry (+15), Affinities (+10)
- ✅ 'Champions Like You' section on Directory (no filters, first page)
- ✅ Match reason badges on recommendation cards
- ✅ Dashboard 'Champions You Might Know' carousel (6 cards, touch scroll)
- ✅ Profile completion prompt when <50% complete
- ✅ Activity tracking: recommendation_viewed event
Champions and events can have photo galleries.
Features:
- ✅ Cp::PhotoAlbum & Cp::Photo models with Active Storage
- ✅ Champion admin: album CRUD, drag-drop uploads, photo management
- ✅ Champion Portal: /photo-albums index and detail pages
- ✅ Lightbox component with keyboard/swipe navigation
- ✅ Dashboard featured photos carousel
- ✅ Event page photo gallery (grouped by album)
- ✅ Activity tracking: photo_album_viewed event
- ✅ 33 controller tests passing
Phase 6: Reporting & Insights ✓ Complete
Analytics dashboards for Community Leaders and staff — proving program ROI. Activity metrics and CSV exports delivered; community health and PDF reports moved to Backlog.
View DocumentationTrack and display key activity metrics.
Features:
- ✅ Total/active Champions counts (MetricsService)
- ✅ Daily activity trends chart
- ✅ Searches, messages, profile views, logins
- ✅ Top connectors table
- ✅ Recent activity feed
- ✅ CSV export for detailed metrics
- Time period filters (quick select + custom range)
Monitor engagement health per community. Moved to Backlog — build user base first.
Features:
- ✅ District breakdown table (MetricsService)
- Formal health scores (composite metric)
- Heat map visualization
- Inactive community alerts
- CL assignment recommendations
Generate and export reports for presentations. Moved to Backlog — build user base first.
Features:
- ✅ CSV export (champions, communities, boards)
- Report templates with branding
- PDF export for presentations
- Scheduled email reports
Phase 7: Career Center ✓ Complete
Self-serve career hub with downloadable resources, curated links, career events, and OCPD partnership.
View DocumentationPublic Career Center page with resources, quick links, career events, and OCPD section.
Features:
- ✅ Public /careers page (no login required)
- ✅ Career resource data model (cp_career_resources table)
- ✅ Resource categories: Getting Started, Career Advice, Networking, Follow-Up
- ✅ Quick links grid (Handshake, LinkedIn Learning, Indeed, Glassdoor)
- ✅ Career events section (events with career_event flag)
- ✅ OCPD partnership section (consultations, resume review, workshops)
- ✅ Sign-up CTA for unauthenticated visitors
- ✅ Career Center in top-level navigation (desktop + mobile)
- ✅ Activity tracking: career_center_view event
PDF download functionality and admin CRUD for career resources.
Features:
- ✅ Download endpoint with counter tracking
- ✅ Staff admin: create/edit/delete career resources
- ✅ PDF upload with Active Storage (drag/drop via pdf_dropzone_controller)
- ✅ Resource search, category filter, status filter
- ✅ Toggle active/inactive (soft remove)
- ✅ Activity event: resource_downloaded
Career event tagging in admin and career center analytics.
Features:
- ✅ Admin event form: career_event checkbox
- ✅ Career Center analytics dashboard (views, downloads, trends)
- ✅ Period filtering (7d/30d/90d/All Time)
- ✅ Career events scope and display
- ✅ Activity event: career_center_view
Phase 8: Notifications & Digests Consolidation ✓ Complete
Unified notification system with consolidated daily/weekly digests.
View DocumentationUnified notification preferences in Settings.
Features:
- Consolidated settings page with all notification types
- Frequency selector (Immediate, Daily Digest, Weekly Digest, Off)
- Per-type toggle organization
- Clear section headers (Messages, Communities, Discussions)
Notification triggers for discussion board activity.
Features:
- Reply to your post notification
- Reply to your comment notification
- Reaction to your content notification
- New post in community notification (CL opt-in)
- In-app notification dropdown integration
Background jobs for digest generation and delivery.
Features:
- Cp::NotificationDigestJob with daily/weekly modes
- Heroku Scheduler configuration
- Monday guard for weekly digests (Heroku daily-only workaround)
- Admin weekly digest bug fix (was running daily)
Dedicated templates for time-sensitive notifications.
Features:
- Join request approved/denied emails
- Verification pending/approved emails
- Support reply notifications
- Community Leader assignment email
- Removed from Settings UI (always immediate)
Phase 9: Onboarding & Initial User Experience ⚡ In Progress
Streamlined first-run experience with progressive disclosure and guided setup.
View Documentationjoin_communities wizard step with community suggestions.
Features:
- Community suggestions based on degrees, location, affinities
- Suggestion cards with icon, name, member count, reason
- Join and decline buttons with AJAX updates
- Step skipped automatically if no suggestions
- Smart step insertion after profile completion
Staff-seeded discussion questions that rotate across community boards to spark engagement.
Features:
- Cp::SeededQuestion model with targeting (global, community_type, affinity_category)
- Cp::SeededQuestionExposure tracking for rotation/cooldown logic
- SeededQuestionSelector service with weighted selection algorithm
- SeededQuestionPublisher service with template interpolation
- 30-day rotation cooldown and max 2 active per community
Lookup Portal admin interface for managing seeded discussion questions.
Features:
- Full CRUD for seeded questions at /champions/seeded_questions
- Filtering by status/target_type and search by title/body
- Preview panel with template variable interpolation
- Activate/deactivate actions for question lifecycle
- Exposure history tracking with community/date details
Analytics to measure onboarding effectiveness.
Features:
- ✅ Signup funnel tracking (email submitted → verified → password created)
- ✅ Wizard completion metrics (started, step viewed/completed/skipped)
- ✅ Seeded question metrics (community suggestions accepted/declined)
- ✅ Admin dashboards at /champions/insights/onboarding and /seeded_questions
- ✅ Period filtering (7d, 30d, 90d, all time)
Auto-generated welcome content for community boards.
Features:
- ✅ WelcomeContentGenerator service with template selection by community type
- ✅ 15 welcome pack templates in config/welcome_packs.yml
- ✅ Markdown-to-HTML conversion using Redcarpet gem
- ✅ Dismissible welcome panel UI with Stimulus controller
- ✅ Member tracking (view count, dismissed_at) for auto-dismissal
- ✅ 7 rake tasks for content management (generate, preview, status, clear, reset)
- ✅ Comprehensive test coverage (26 tests)
Multi-email drip campaign for new Champions.
Features:
- Welcome email (Day 0, immediate)
- Profile nudge (Day 2, skip if complete)
- Directory introduction (Day 5, skip if searched)
- Community engagement (Day 10, skip if posted)
- Event participation (Day 15, skip if registered)
- 30-day check-in (Day 30, always send)
- Skip logic for conditional delivery
- Admin progress visibility in Lookup Portal
Allow community discovery pre-verification. Verification unlocks contribution, not belonging.
Features:
- Wizard community step shown to unverified champions
- District/industry/affinity suggestions without verification
- College/major suggestions with BUID linkage or invitation
- Community pages viewable pre-verification
- Directory and messaging require verification
- Content read-only until verified (news, events, discussions, photos)
- Verification teaser messaging for gated contribution features
Phase 10: Connections ✓ Complete
Relationship-first communication: connection requests, structured outreach, and rate-limited messaging.
View DocumentationCore tables, champion connection preferences, settings UI.
Features:
- cp_connection_requests table (requestor, requestee, type, message, status)
- cp_connections table (canonical pair, linked thread, connected_at)
- Connection preferences on champion (types, weekly cap, pause, almost-alumni toggle)
- Connection Settings UI in Champion Settings page
Send, receive, accept, ignore, and cancel connection requests.
Features:
- Connect button on profiles with request modal
- Connection type selector + freeform message
- Rate limiting: 10/day requestor, configurable weekly requestee cap
- Requests tab with incoming/sent sub-tabs
- Accept, Ignore (silent), Cancel actions
- Notifications for new requests and acceptances
Transform Messages into Connections with two-tab inbox.
Features:
- Rename Messages to Connections in navigation
- Two-tab layout: Requests + Connections
- Gate messaging behind accepted connections (support exempt)
- Connection thread header with type badge and connected date
- Disconnect flow with re-request capability
Surface connection availability in Directory and Profiles.
Features:
- Directory filter: Open to (connection type multi-select)
- Subtle open-to-connect indicator on directory cards
- Profile connection section with types and Connect button
- Quick-access settings link on Connections page
Migrate mutual contacts to connections, remove Contacts system.
Features:
- Rake task: mutual contacts become Connections with threads
- Remove Add to Contacts button and ChampionContact model
- Replace My Contacts directory filter with My Connections
- Update compose modal to use connections endpoint
- Remove contact_added activity/notification types
- Update privacy settings labels to connection language
- Update AlumniLikeMeService to use connected_champion_ids
Staff visibility into connection program health.
Features:
- Connection Analytics page with overview cards, daily trends chart, and period filtering
- Request status breakdown (accepted/pending/ignored/cancelled) with acceptance rate
- Connection type breakdown with horizontal bar visualization
- Super-connector leaderboards (Most Connected, Most Active Requestors, Most Welcoming)
- Per-champion connection stats in admin detail view (connections, requests, acceptance rate, type breakdown)
- Average response time calculation with smart unit display
- Sidebar nav link for Connection Stats under Analytics section
Phase 11: Champion Role Dashboard Card ✓ Complete
Daily role-aligned idea packs on the dashboard, roles landing page, and admin CRUD for role ideas.
Content model, seed data, and pack generation service.
Features:
- Cp::RoleIdea model with role, target, status, priority, template interpolation
- Cp::RoleIdeaPack and Cp::RoleIdeaPackItem models for daily pack persistence
- RoleIdeaPackService with weighted random selection, anti-repeat window, timezone awareness
- 115 seed ideas across all roles authored from canonical source documents
- Rake tasks for seeding, stats, and old pack cleanup
Dashboard card with State A (no role) and State B (role selected) variants.
Features:
- State A: Discover Your Role quiz CTA for champions without a role
- State B: Daily idea pack with primary + secondary ideas and CTAs
- Community Leader variant: mixed pool of role + CL-specific ideas
- Turbo Stream refresh endpoint for 'Try different ideas' button
- 6 new activity event types for role card interactions
Public roles page and staff management interface.
Features:
- Roles page showing all 4 Champion roles with source document descriptions
- Admin CRUD with filtering by status/role/target, search, and pagination
- Activate/deactivate/archive status management
- Admin sidebar link with active count badge
Phase 12: Source-Alignment Review ✓ Complete
Full audit of Champion Portal user-facing text against 20 canonical source documents. Inclusivity framing, copy alignment, and vocabulary standardization.
9 string replacements and 3 email template rewrites for source-aligned copy.
Features:
- Invitational CTAs replacing transactional language (Sign Up → You're Invited)
- Warm registration heading (Become a Champion → Welcome Home, Bruin)
- Community vocabulary replacing system/platform/network language
- Confirmation, verification, and invite emails rewritten with belonging-first tone
- Inclusivity framing: external = alumni/Bruins, internal = Champion as celebration
FAQ, empty states, flash messages, landing page, and wizard copy.
Features:
- FAQ 'portal' → 'community' (8 replacements)
- 15 empty states rewritten with warmth and CTAs
- 8 flash messages warmed (sign-in/out, boards, communities)
- Landing pillars renamed: Belong / Gather / Inspire (identity arc)
- Gatekeeping messages softened (access-denied experiences)
- Wizard header and connections step copy refined
- No platform proper noun — 'your community' / 'your alumni community'
Phase 13: Progressive Dashboard & Engagement Tiers ✓ Complete
Journey-aware, tier-conscious dashboard. Replaces static 13+ section layout with progressive disclosure and narrative-driven engagement.
View DocumentationThree-tier model (Member/Champion/CL) derived from existing data; 8-stage journey engine cached in DB.
Features:
- ✅ Cp::Tierable concern with engagement_tier, compute_journey_stage, has_contributions?
- ✅ journey_stage integer column on cp_champions (migration + index)
- ✅ Cp::DashboardVisibility service — stage → section visibility map, tier-based sidebar ordering
- ✅ rake journey_stage:recompute_all backfill task
- ✅ 49 tests (25 Tierable + 24 DashboardVisibility)
New Cp::HomeController + /home route with stage-aware views and 3-tier visual card hierarchy.
Features:
- ✅ Cp::HomeController with selective data loading per stage
- ✅ GET /home route + feature-flagged root switching (progressive_dashboard flag)
- ✅ 17 view files: show.html.erb, _hero.html.erb, 3 card tier partials, 7 sidebar partials, 5 stage partials
- ✅ Cp::HomeHelper#thread_other_champion with multi-API fallback
- ✅ 9 HomeController + 3 helper/view tests
- ⏸️ Compact mobile hero, 'what's new' sub-line, mobile sidebar fold — deferred to 13.3+
Unified discussions/news/photos feed with recency + engagement scoring.
Features:
- ✅ Cp::ActivityFeedService with mixed-type scoring (recency, engagement, unseen, community bonuses)
- ✅ last_dashboard_visit_at column (migration) for unseen detection
- ✅ Feed view with featured item + compact list (top 7 items)
- ✅ FeedItem struct with polymorphic content support (discussions, news, events, photos)
Stage partials for onboarding, exploring, connecting, and engaged member journeys.
Features:
- ✅ Onboarding stage: checklist with per-item routing to wizard steps
- ✅ Exploring stage: community type grid + recommendations
- ✅ Engaging stage: activity feed + career resources + suggestions
- ✅ Language updates: alumni-first copy, hope-oriented empty states
- ✅ Connections UX: restriction framing, wizard reorder
- ✅ Sidebar profile completion through engaged_member stage
Contextual prompts, champion recognition badges, and role ideas in digest emails.
Features:
- ✅ ContextualPromptGenerator service (role-aware, data-driven prompts)
- ✅ Role card contextual prompt rendering with amber 'Now' badge
- ✅ Alumni Champion SVG icon badge (directory, discussions, comments, profile)
- ✅ Role ideas in daily/weekly digest emails
- ✅ resolve_contextual_cta_route helper for deep-linking
New member welcoming for community leaders.
Features:
- ✅ New Members to Welcome prompt for community leaders
- ✅ Welcome tracking via activity events (cl_welcome_sent)
- ✅ Community Leader stage integration
Contextual nudges guiding Tier 1 Members toward Champion opt-in, plus 3-page Champion Role microsite.
Features:
- ✅ Champion Discovery Page (/champion-info) — educational 'What & Why' page, no inline role management
- ✅ Champion Role Microsite: /champion-info + /roles + /roles/:role with shared navigation
- ✅ Shared microsite nav partial with tab indicators and dashboard link
- ✅ Role detail page with management actions (retake quiz, change role, remove role)
- ✅ Route consolidation: removed duplicates, added DELETE /roles/remove
- ✅ All entry points updated (dashboard, profile, wizard, nudge → microsite pages)
- ✅ ChampionNudgeEngine service with 5 trigger definitions and JSONB tracking
- ✅ Dashboard nudge card with dual CTAs (quiz + learn more) and dismiss
- ✅ Post-action inline nudges after news/event submissions and 3+ comments
- ✅ 7 new activity event types for nudge + role management analytics
- ✅ Consistent max-w-3xl container with standardized padding across all microsite pages
Dashboard freshness signals, milestone celebrations, connections sidebar redesign, and community activity badges.
Features:
- ✅ What's New summary bar with content counts since last visit
- ✅ Milestone celebrations system (8 types, auto-detection, gold celebration banner)
- ✅ Connections sidebar redesign (people-first with photos instead of message threads)
- ✅ Community activity badge labels (new posts + upcoming events per community)
- ✅ Role card visibility expanded to exploring+ stages
Phase 14: Alumni Content Submissions ✓ Complete
Champions submit news and event ideas through forms; staff review, promote to draft, publish directly, or decline with feedback. Full conversation threads, notifications, and attribution.
Status enums, submission fields, thread/message models.
Features:
- submitted/declined status enums on NewsPost and Event models
- Submission tracking fields (submitted_by_champion_id, decline_reason, timestamps)
- ContentSubmissionThread model with polymorphic content association
- ContentSubmissionMessage model with polymorphic sender and read tracking
News and event submission forms, CTAs, submission history.
Features:
- News submission form at /news/submit with community pre-selection
- Event submission form at /events/submit with community dropdown
- Submit CTAs on news index, events index, and community content sections
- Submission history at /my-submissions with status badges
- Thread detail view with conversation history
Staff review interface with tab-filtered queue and actions.
Features:
- Admin queue at /champions/content_submissions with Pending/In Conversation/Promoted/Declined tabs
- Promote to Draft (redirects to standard edit form)
- Publish Directly with confirmation
- Decline with reason prompt
- Reply on conversation threads
- Admin sidebar link with pending count badge
Email notifications, in-app notifications, and 'Submitted by' attribution.
Features:
- 4 notification types: submission_reply, submission_promoted, submission_published, submission_declined
- ContentSubmissionMailer with new_submission, champion_reply, and submission_published emails
- Attribution display on published news cards/detail and event cards/list/detail
Phase 15: Design Consistency Pass ✓ Complete
View-only polish pass aligning all cp/ views to the design tokens established by the Phase 14 /home dashboard. No model, route, or controller changes.
View DocumentationAll card/panel containers converted from rounded-xl/2xl to rounded-md.
Features:
- rounded-md on all card/panel containers across 13 targeted pages
- Icon containers and chat bubbles preserved (documented exceptions)
- Consistent border border-gray-200 and shadow-sm across all cards
bg-gray-50 border-b headers added to show-page cards.
Features:
- Card header pattern (bg-gray-50 border-b border-gray-100 px-5 py-3) applied to targeted show pages
- Section titles promoted to header bars with justify-between View All links
- Body content starts in px-5 py-4 wrapper
ALL CAPS section/form labels removed; standardized to Title Case.
Features:
- uppercase tracking-wider and uppercase tracking-wide removed from all section/form labels in cp/ views
- Label text updated to Title Case (not just CSS class removal)
- Month abbreviations in event date badges preserved (documented exception)
Cosmetic content-area gradients replaced with flat design tokens.
Features:
- CTA bars on news/index and events/index use bg-gray-50 flat background
- Community section panels use bg-white
- FAQ category card headers use bg-gray-50
- Full-color promotional gradient blocks and role micro-gradients preserved (documented exceptions)
Phase 16: Legal & IT Compliance ✓ Complete
Compliance implementation covering legal policy pages, consent tracking, education privacy controls, true account deletion, CAN-SPAM email compliance, security headers, and age attestation.
View DocumentationIn-app policy pages (ToS, Privacy, Community Guidelines, Cookies) with versioned consent tracking at registration and re-consent mechanism.
Features:
- ✅ Policy pages served at /terms, /privacy, /community-guidelines, /cookies
- ✅ cp_policy_acceptances table for audit trail
- ✅ Cp::PolicyVersion with CURRENT_TERMS / CURRENT_PRIVACY version constants
- ✅ Registration consent checkbox (ToS + Privacy Policy)
- ✅ Consent timestamp and version tracking on champion record
- ✅ Re-consent flow when policy version changes
- ✅ Footer links to all policy pages
- ✅ 37 new tests + champion model consent assertions
Privacy enum for education records with 3 levels (show all, hide year, hide all). Ripples into directory, search filters, and matching services.
Features:
- ✅ education_privacy enum on cp_champions (show_all, hide_year, hidden)
- ✅ 6 views updated to respect education privacy level
- ✅ Directory search/filter exclusion for hidden education
- ✅ AlumniLikeMe and CareerConnect services updated
- ✅ Settings page control for education visibility
- ✅ Self-view shows full education with privacy indicator
True account deletion replacing privacy-only stub. Discussion posts anonymized to 'Former Member', PII purged. Plus 'Download My Data' JSON export.
Features:
- ✅ Confirmation page with CONFIRM text verification
- ✅ Cp::AccountDeletionService — full PII purge in transaction
- ✅ Discussion posts anonymized to 'Former Member'
- ✅ Messages, memberships, connections removed
- ✅ Confirmation email to original address
- ✅ 'Download My Data' JSON export in Settings
- ✅ Rate-limited to 1 export per 24 hours
Unsubscribe language in all email footers, full physical address in text emails, List-Unsubscribe headers.
Features:
- ✅ HTML footer: 'Manage preferences or unsubscribe' link text
- ✅ Text footer: full physical address (1900 Belmont Blvd)
- ✅ List-Unsubscribe and List-Unsubscribe-Post headers on non-transactional emails
- ✅ Transactional emails exempted (Devise confirmations, password resets)
- ✅ Shared text footer partial for all 27 text email templates
Content Security Policy (report-only), Permissions-Policy, Referrer-Policy headers enabled.
Features:
- ✅ CSP in report-only mode with Google Fonts, OAuth, Tailwind whitelisted
- ✅ Permissions-Policy restricting camera, microphone, geolocation, payment, USB
- ✅ Referrer-Policy: strict-origin-when-cross-origin (production)
- ✅ HSTS via existing force_ssl = true
- ✅ 11 integration tests for security headers
Age confirmation checkbox at registration (18+). Records attestation timestamp.
Features:
- ✅ Age confirmation checkbox on registration form
- ✅ age_confirmed_at timestamp on champion record
- ✅ Validation prevents registration without confirmation
Phase 17: Champions Landing & Legacy Cleanup ✓ Complete
Public Champions landing page (alumnichampions.com redirect target), generic landing refresh, legacy signup retirement, and data catalog.
View DocumentationPublicly accessible /champions page that educates visitors about the Alumni Champions program.
Features:
- ✅ Public controller (no auth, ApplicationController pattern)
- ✅ /champions route as alumnichampions.com redirect target
- ✅ Adapted content from /champion-info for anonymous audience
- ✅ Three engagement tiers + four role cards (informational)
- ✅ Social proof: true Champions count (role selected) + city count
- ✅ OG meta tags for social sharing
- ✅ Signed-in redirect to /champion-info
- ✅ 10 tests, 26 assertions
Update / landing to articulate two-tier value proposition (join network vs. become Champion).
Features:
- ✅ 'What You'll Find Inside' — 4 feature cards (Directory, Communities, Events, Conversations)
- ✅ 'For Alumni Who Want to Do More' — Champions teaser linking to /champions
- ✅ Kept hero visual treatment + social proof + final CTA
- ✅ Removed inline footer (layout footer)
- ✅ 10 tests, 22 assertions
Remove public /signups/* flow, preserve ChampionSignup data conversion pipeline.
Features:
- ✅ 301 redirects: /signups/new and /signups/:id → /champions
- ✅ Removed public controller, views, mailer, initializer, rake task
- ✅ Preserved SVG partials (_seal, _icon) used by staff views
- ✅ Preserved conversion pipeline, staff management, model
- ✅ 2 redirect tests, 4219 total runs, 0 failures
Document ChampionSignup data pipeline, preserve staff management tools.
Features:
- ✅ Verified to_champion_attributes field mapping (9 fields)
- ✅ Legacy data notice on staff signups index
- ✅ CHAMPION_SIGNUP_SYSTEM.md updated with retirement notice
- ✅ Data usage catalog verified against code
Phase 18: Education Data Model Migration ✓ Complete
Migrate alumni education data from Degree/Major/College to a person-centered Education + Areas of Study model with backwards-compatible API rollout.
View DocumentationFinalize field mapping, derivation rules, and API compatibility contract before schema work.
Features:
- Field-level mapping spec (legacy -> new)
- UG/GR derivation and degree-level rules
- Granting school vs current school semantics
- Backward-compat API contract definition
Create Education and EducationAreaOfStudy schema and associations alongside legacy tables.
Features:
- Educations table + indexes (with DB-level FK to alumni.buid)
- Education areas of study table + indexes
- Alumni associations to education records (has_many :educations, has_many :education_areas_of_study through: :educations)
- Duplicate prevention and model validations (unique source_education_id per buid; globally unique person_area_of_study_id)
- Auto-derived degree_level and area_of_study_name_normalized on save
Stand up new CRM education + areas-of-study CSV importers with background-job pipeline, polled status page, and downloadable gap CSV.
Features:
- Two new CRM CSV importers (educations, areas of study) with idempotent upserts
- Background-job pipeline (scan + apply) with persistent EducationImportBatch
- Polled status page with preview, commit, gap CSV, and cancel
- Soft-school rule + concentration-level fallback to 'unknown'
- Skip-reason transparency for areas of study
- Granting-school college seed (29 historical/current codes)
- BUID + BQID surfaced in gap CSV exports for Advancement Services
- Auto-refresh on commit redirect (status flips to running before enqueue)
- CSP allowance for jsdelivr + cdnjs CDN-loaded scripts
Centralize old-field derivation (ug_*, gr_*, pref_college) from the new model.
Features:
- Alumni::EducationProfile presenter (Education-first with Degree fallback per alumni)
- Value objects: Privacy, Entry, CurrentSchool with privacy auto-inference from cp_champions.education_privacy
- Alumni#recent_degree and #graduation_years delegate to presenter
- AlumniHelper#generate_checkin_data uses presenter to_export_hash
- Core Lookup + CP views swapped: alumni/show, alumni/search, cp/directory, cp/profile
- Matching services swapped: CommunityMatching, AlumniLikeMe, CareerConnect
- 23 presenter unit tests + 5 Degree/Education parity tests
Preserve current lookup API payloads while moving internals to the new education model.
Features:
- Api::V1::AlumniSearchController#serialize_alumni routes through Alumni::EducationProfile (32-key check-in blob contract preserved)
- Api::AlumniController legacy typeahead routes through presenter (4-key recent_degree contract preserved)
- Strictly-additive `_source` field on both endpoints (education | degree_fallback | none)
- EducationCoverageService: education_coverage_pct, legacy_fallback_pct, unmapped_school_name_count
- Internal /api/v1/education_coverage endpoint (API-key authenticated)
- Admin /settings/data_health dashboard with unmapped-schools drill-down + CSV download
- alumni:legacy_fallback_buids rake task (stopgap until 18.7 surfaces this as a UI filter)
Move Degree Stats and engagement analytics aggregations to a shared Education::AggregateScope query object (Education-first w/ Degree fallback). Stats UI gets a side-by-side ?source=legacy|education toggle and coverage banner so parity is visible before cutover.
Features:
- Education::AggregateScope query object (UNION SQL)
- Parity test suite (degree-derived vs education-derived counts)
- Alumni model scopes migrated invisibly (with_degrees / filter_by_college / filter_by_fiscal_year)
- StatisticsController, EngagementStatsController, EngagementStats::* services migrated
- Side-by-side ?source=legacy|education toggle on stats pages
- Coverage banner driven by EducationCoverageService
- New aggregate_stats_v2:* cache namespace (5-min TTL during validation)
Migrate the data-export surfaces (Alumni CSV, Event RSVP CRM converter, AlumniFilterService) from legacy degree joins to the AggregateScope + EducationProfile stack established in 18.6.
Features:
- Csv::AlumniExporter migrated (preserves ug_*/gr_* column contract via EducationProfile.to_export_hash)
- Csv::EventRsvpConverter migrated (UG/GR classification)
- Tools::EventConverterController migrated
- AlumniFilterService population logic moved behind AggregateScope
- CSV contract tests (headers + values match pre-migration fixtures)
- Cache TTL bumped to 1-hour
Move per-enrollment current-student data (school, program, intended degree, expected graduation year, per-record student_status) out of denormalized alumni columns and into the educations table. Retire Csv::CurrentStudentImporter in favor of a single education-feed pipeline.
Features:
- Add educations.expected_graduation_year + educations.student_status
- Education importer accepts preferred_year + student_status columns
- alumni.student_status becomes a one-way ratchet (awarded never downgrades)
- Current Enrollment display reads from educations.where(date_issued: nil) — supports BS-holder enrolled in MS
- Retire Csv::CurrentStudentImporter + Settings::CurrentStudentsController + UI
- Drop current_school_code, current_program_desc, intended_degree_code, expected_graduation_year from alumni (separate deploy)
Freeze the legacy degrees/majors/colleges read path: retire the side-by-side stats toggle and coverage banner, migrate every remaining per-record and filter read onto Education::AggregateScope + Alumni::EducationProfile, and keep the EducationProfile fallback + physical tables until a later cleanup tag (coverage ≥ 99%).
Features:
- ✅ Group B: side-by-side ?source toggle, coverage banner, and aggregate_stats_v2:* cache namespace removed; EngagementStats services no longer accept source:
- ✅ Group C: all per-record alumni.degrees reads migrated to EducationProfile / AggregateScope (stats tables, verification, champion show, community detection/eligibility, exporters, RSVP converter, like-me scoring)
- ✅ Filter joins migrated: filter_by_name year filter, directory college/grad-year facets, career-connect cluster match, community matching college/major
- ✅ Data-driven reads use Privacy::NONE; display reads keep inferred privacy
- ✅ Group A: areas of study (majors/minors/concentrations) surfaced under each education entry on Lookup + Champion Portal profiles
- ✅ Group D: legacy degree write paths frozen (Degree.writes_frozen?; alumni/banner importers + settings controller no-op the degree branch, alumni records still created/updated)
- ✅ Major filter migrated off Degree: education_areas_of_study.major_code (CRM 'Area of Study: External Id') now backs Alumni.filter_by_major
- ✅ Live progress UI for Education + Areas of Study CRM imports (real-time progress bar replacing meta-refresh)
- ⏳ Later cleanup tag: deprecate Degree model + drop the legacy degrees table once coverage ≥ 99% (colleges/majors retained as active reference tables)
Phase 19: Alumni Champions Signup v2.0 ⚡ In Progress
Rebuild the public Champion signup flow at alumnichampions.com as a server-side multi-step wizard in the public/ namespace. Adds college/major, industry/job title, affinity steps, quiz, and role selection. Restores mailer and confirmation page. 19.6 was a post-feedback UX revision pass. 19.7/19.8 move completion up to a single Congrats/Profile page and replace the role-naming quiz with an interest spectrum.
View DocumentationAdd 7 new columns to champion_signups, wire domain-scoped routes under public/ namespace, scaffold the controller with all step stubs, and render the landing page.
Features:
- Migration: add college_code, major_code, industry, job_title, affinity_codes (jsonb), affinity_other, source to champion_signups
- Routes: resource :champion_signup under module: 'public' inside alumnichampions.com domain constraint
- Controller: Public::ChampionSignupsController < PublicController with all step stubs
- Restore config/initializers/champion_signup.rb
- Views: new.html.erb container, _header, _progress_bar, _landing (4 role cards + CTA)
Steps who_you_are, belmont_experience, and where_you_are are live with simplified data capture. Record is created on step 1 and tracked via session.
Features:
- _who_you_are: first/last name, maiden-name toggle, graduation year(s) free text, email, required ZIP, optional phone
- _belmont_experience: single free-text belmont_background field for college(s)/major(s)
- _where_you_are: profession capture with employment status + company/job/industry
- Flow scaffolding: affinities + role stub pages to support You → Belmont → Interests → Job → Role testing
- Controller guards for session-tracked signup across all signup-dependent steps
Role hub (direct pick or 7-question quiz), quiz results with recommendation, light interest capture, and a role-celebrating confirmation page — all under the public/ namespace.
Features:
- _role: role hub with relational framing and 3 paths (Take quiz, I already know, Skip for now); role cards hidden until explicitly requested
- _quiz_question: one question per page (question1–question7), shuffled options, answers persisted to champion_signups.answers
- _quiz_results: ChampionQuizService recommendation, seal + narrative + blend breakdown, routes back to role hub to confirm
- _affinities: light interest capture (belonging_categories checkboxes + belonging_note)
- Role submission resiliency: signed signup_token fallback prevents session-loss redirects to landing
- show: confirmation/thanks page celebrates the chosen role (seal + title + description)
- 44 controller tests; full suite passing
Full affinity browse/search on the Interests step, source tagging, and email delivery on completion. Replaces the 19.3 light-interest checkboxes with the full affinity dataset and restores the signup mailer.
Features:
- _affinities: full browse/search (reuses cp-affinity-selector) — affinity_codes + affinity_other + belonging reflection (belonging_note); no lifestage_interest / belonging_categories capture
- Source tagging: source='champion_signup_v2' set on step 1 creation
- Completion: welcome + admin emails sent via deliver_later on the final role step (both confirm and skip paths)
- ChampionSignupMailer restored, delegating role copy to ChampionRoleService (not an inline hash); per-instance default_url_options
- Staff queue: v2 source badge + source filter dropdown
- Tests: public controller + mailer + staff filter; full suite passing
Confirmation UX and staff review surfaces finalized, including secure no-role follow-up and expanded queue filters.
Features:
- Public confirmation: role branch now includes full role activity list + next-steps card
- Public confirmation no-role branch: optional follow-up CTA is shown only when a valid signed signup_token is present
- Role completion redirect includes signed signup_token for secure continuation
- Staff queue filters: role status, BUID link status, affinity data presence, recency buckets, contactability, and specific affinity
- Staff show: dedicated v2 section for source, college_code, major_code, industry, job_title, affinity_other, and affinity_codes
- Affinity rendering fallback: show affinity name when resolvable, otherwise raw code
- Tests added for confirmation branches and expanded staff filtering; full suite passing
Post-feedback UX polish based on user testing of the full signup flow.
Features:
- Step 2 heading changed from 'Your Belmont story' to 'Your Area of Study'
- Quiz results: replaced two-hop confirmation with direct 'Confirm this role' (POST) + 'Select a different role' link
- Role hub green banner copy updated to match new flow intent
- Where You Are: added 'What are you up to these days?' free-text (current_season_note) shown only when 'I'm not currently working' is selected
- Migration: add_column :champion_signups, :current_season_note, :text
Move signup completion up to a single Congrats/Profile page reached right after contact info. Drops the Belmont Experience step, and the same page doubles as a returnable, on-demand-link 'update your info' profile for the remaining optional sections.
Features:
- Drop belmont_experience step from the flow
- New flow: who_you_are → Congrats/Profile page → affinities → where_you_are → interests
- show.html.erb becomes the unified Congrats/Profile page: first-visit 'You're in!' framing, hub linking to the 3 optional sections, and the return-visit 'update your info' page
- Welcome email sends immediately at step 1 completion, with a short-lived on-demand profile link (not permanent)
- New 'email me a link' request flow (landing page form + mailer action) — generic response regardless of match, to avoid email enumeration
- Delayed nudge email (~3 days) if none of the 3 optional sections are completed
- Education-record note on the profile page, shown only when buid is already linked (high-confidence match)
Replace the single-winner role quiz with an interest spectrum across the same 4 areas, shown on both the public results screen and the staff admin side.
Features:
- Reuse the existing 7 quiz questions; results screen shows a 4-area breakdown (bars) instead of crowning one role
- Plain-language area names with role icons (e.g., 'Bringing People Together') — no role-title text on the public flow
- Remove the direct pick-a-role path — Interests becomes quiz-only
- Migration: add interest_scores (jsonb) to champion_signups, persisted when the quiz finishes
- Backfill rake task for existing signups with answers but no interest_scores; run once on each environment after migrating
- result_role still persisted as the top-scoring area for backward compatibility
- Staff show: new breakdown section (bars/icons per area), computed on the fly when interest_scores isn't stored
- Staff queue: new filter — area + minimum score threshold
Phase 20: Feature Flags & Phased Launch ✓ Complete
Staff-toggleable feature flags (News, Photos, Events, Champion Role, Your Impact) so the portal can launch as an approvable stripped-down core and re-introduce features one at a time. Flags hide champion-facing surfaces only — data and staff tools stay intact.
View DocumentationDB-backed feature flags with a Champion Admin toggle UI. Infrastructure only — no gating yet.
Features:
- cp_feature_flags table + Cp::FeatureFlag model (5-key closed set)
- Resolution order: ENV override → cached DB value → false (fail-closed)
- cp_feature_enabled? + require_feature! helpers on Cp::BaseController
- AlumniNetwork::FeatureFlagsController + toggle UI at /champions/feature_flags
- Per-environment defaults: enabled everywhere except production
- 15 tests (model resolution + admin toggle/guard); full suite green
Controller guards, community-page reflow, nav/footer links, dashboard events sidebar, activity-feed filtering.
Features:
- require_feature! on News/Photos/Events + submission controllers
- Community show reflows cleanly with each section hidden
- DashboardVisibility events integration; nav/footer/mobile-nav guards
- Activity feed / What's New excludes gated items
Roles microsite, dashboard role ideas + role card, wizard role-step skip, Your Impact card.
Features:
- Roles microsite + controllers gated; role-quiz wizard actions gated
- Role badge hidden portal-wide via champion_role_badge helper
- DashboardVisibility role_ideas + champion_nudge (champion_role) + your_impact
- Your Impact sidebar gated (independent flag); role card/hero/profile role UI hidden
Phase 21: Alumni Opportunities ✓ Complete
Config-free, staff-authored forms for concrete things an alum can sign up to do — represent Belmont at an admissions fair, host a watch party, speak to prospective students, host a send-off. Shareable public links on alumnichampions.com, a returning-signup surface on the profile hub, and a staff queue + export. Entirely within the signup ecosystem; no belmontalum.com surface.
View Documentationopportunities + opportunity_responses tables and their models. Nothing routed or publicly reachable.
Features:
- Opportunity: 5-type closed field set, field-array validation, slug generation + immutability
- Three integrity rules in the model: frozen field keys, soft-deleted fields, slug locked once live
- activated_at column so the deactivate → fix → reactivate rollback can't unlock a shared slug
- OpportunityResponse: ZipCode.lookup city/state/district, dynamic answer validation, email auto-link
- 49 model tests
Staff author opportunities in the admin — copy, questions, and the live/off toggle — instead of a developer ticket and a deploy.
Features:
- Full CRUD + preview, activate/deactivate, duplicate
- field_rows_controller.js — add/retire/restore/reorder question rows
- Preview renders the real public form off the real partials, disabled and unsubmittable
- OpportunityMarkdown — escape_html + safe_links_only for staff-typed copy on a public page
- Integrity UX: permanent keys, Retire instead of delete, read-only slug once shared, delete blocked once answered
The CMS relocates into the Signup Admin shell and the responses queue lands beside it, so staff do the whole job in one place.
Features:
- CMS moved from /champions/opportunities to /champion_signups/opportunities (portal_admin only)
- New Opportunities sidebar section: editor ('N live') + Responses (unreviewed badge)
- Responses queue filtered by opportunity, district, on-file vs. new, review state, date; sortable + paginated
- reviewed_at is read/unread only — stamped on open, with a manual mark-unreviewed toggle
- NotifyOpportunityResponseJob → in-app + web push to admin + can_portal_admin, no email
- Csv::OpportunityResponseExporter — per-opportunity dynamic columns incl. retired questions, buid + contact_id
The anonymous-capable public form at alumnichampions.com/opportunities/:slug, its thank-you page, and the conversion loop into the existing signup flow.
Features:
- Public::OpportunitiesController#show/#create/#thank_you; inactive or unknown slug 404s
- Honeypot reuse + rack-attack per-IP throttle (5 per 30 min)
- Privacy disclosure stamped per 19.11 — no checkbox, submitting is the acknowledgment
- Identical thank-you copy for matched and unmatched submitters; never links into the profile hub
- Conversion CTA carries a signed response token → pre-filled signup, linked back on save
Returning signups see live opportunities on their profile hub, pre-filled and attributed.
Features:
- 'Ways to show up' section on the hub; active only, hidden entirely when nothing is live
- Links carry the profile token so the form pre-fills and the response attributes to the signup
- ChampionSignupEvent opportunity_viewed / opportunity_submitted for known signups
Three limits the 21.1–21.5 MVP left in place, each hit by staff running live forms. Questions could only take one answer. Activating a form was the same switch as advertising it, so a board-only or newsletter-only opportunity was impossible. And responses only reached people with a Lookup Portal account, which the person who owns a given opportunity often isn't.
Features:
- Checkbox (multi-select) field type — the sixth, and the first whose answer is an array
- Listed vs. link-only: an unlisted opportunity serves its direct link and appears nowhere
- Per-opportunity notification email list — any address, no portal account needed
- Email carries form answers plus the linked Champion record: grad year, location, affinities, industry, and the matched alum's degrees, BUID and BQID
- Email matches are labeled unconfirmed — a guess never reads as a proven identity
- Email is opt-in per form, so 21.4's inbox-flood protection survives intact
Staff were typing the three facts an alum most needs — when, where, and what we're actually asking — as hand-written lines at the top of the intro copy. That made them formatted differently on every form, present nowhere except the form itself, and impossible to sort or expire by. An alum who said yes to an event had no record of the date anywhere but the page they had just navigated away from.
Features:
- When / Where / The ask as real fields — a start, an optional end, and a free-text override for dates a timestamp can't express
- One details block on the form, the thank-you page, the hub cards, and both emails; a blank field is left off entirely
- A dated opportunity closes itself at the end of its last day — the link 404s and it leaves the profile hub
- The thank-you page outlives the expiry, so nobody loses the receipt they submitted on the final day
- Staff see an Ended badge instead of Live, and an expired opportunity stays editable
- Hub lists soonest-first rather than alphabetically
- Optional confirmation email to whoever signed up — opt-in per form, and carrying nothing drawn from a Champion record we only matched by email
21.8 made the event details structured; it rendered them as a plain label column, which read as an afterthought beside the Alumni Network's event listings. And because Where is one text field, a real venue arrived as a run-on grey line with the street address crowding out the name anyone actually reads.
Features:
- Upcoming events and Ongoing opportunities split into two groups — an event is decided by whether you're free, a standing offer by whether you want to
- No date now simply means Ongoing, derived rather than typed — 21.8's "say it differently" box is gone, and a note can no longer disagree with the dates that close the form
- Dated opportunities get a blue date bar, replacing a heart icon that was the same on every row
- The first line of Where is the venue and shows in bold; the rest is the address, and it's what produces a View Map link
- "Various locations" still works untouched and gets no map link — staff decide by whether they type an address
- Calendar and map-pin icon rows on the form, preview and thank-you pages, matching the event page treatment
Phase 22: Staff Notification Inbox & Signup CRM Feedback Loop ✓ Complete
Two problems with one cause: the signup ecosystem grew fast while the staff-facing plumbing stayed pointed at the Alumni Network. StaffNotification had been written to by six jobs since Phase 1 with no controller, route or view — every row surfaced only as a transient web push, and missing the push lost it forever. Meanwhile the navbar bell showed uncleanable aggregate counts, six of its eight items linking into the held /champions/ surface. Part B routes signup-reported contact, employment and student-org updates into the existing CrmDataChange export pipeline, which no signup has ever written to. Sequenced AFTER phase 24, which supplies the employments table 22.4 diffs against.
View DocumentationGive StaffNotification the UI it never got — a real per-user inbox with a receive → clear → gone flow.
Features:
- NotificationsController at /notifications — unnamespaced Lookup Portal core, not champion_signups/: the feed spans both surfaces and the bell renders in layouts/_navbar, which all five Lookup Portal layouts include
- /notifications is free here — Cp::NotificationsController owns the same path inside the belmontalum host constraint and never matches on *.alumnilookup.com
- StaffNotification::SURFACES maps every type to Champion Signup or Alumni Network — one place to declare it, with a model test that fails the build on an unmapped type
- Per-user isolation enforced at the lookup (current_user.staff_notifications.find), not the view — a bare find would let user A clear user B's inbox
- mark_read stamps read_at AND clicked_at then forwards to the notification url; mark_all_read deliberately sets read_at only, so clicked_at means 'I went and looked'
- Surface-major ordering with recency inside — sorting by recency alone repeats both group headings on every page
- Filter by surface, type and read state; paginated; per-group Mark all read
- 32 tests (22 controller, 10 model)
One dropdown, two halves that must not be merged: a clearable feed above the standing queues.
Features:
- New (unread StaffNotifications, clearable) above Pending work (ActionItemsService, relabeled as counts that resolve themselves)
- Badge counts unread notifications only — priority_count summed support/verification/moderation queues, all on the held surface and none clearable from the bell
- priority_count and discussions_needing_moderation_count removed outright; the badge was their only caller
- surface: added to the ActionItem struct; a third surface :lookup for CRM changes, which is neither signup nor Alumni Network
- Alumni Network items collapse under one de-emphasized heading, with a test tying that grouping to the actual /champions/ paths so the two can't drift
- Footer link retargeted from alumni_network_root_path to the inbox (the label rename is Phase 23's job)
- 21 tests (14 bell view, 7 service)
Wire the mature CrmDataChange pipeline to the signup flow. Linked signups only — a change Advancement Services can't attach to a constituent is unusable.
Features:
- ChampionSignupCrmLogger resolves the linked Alumni by buid and logs only meaningful, un-logged changes; called from all three public save points
- change_source champion_signup stays distinct from champion_portal — Advancement Services needs to know which product the alum used
- Explicit field mapping: zip_code → the alumni zip column, and an email matching ANY of alumni's five email columns is suppressed
- street has no alumni column to diff against, so it logs with a blank old_value rather than being dropped
- app_originated is now derived from SOURCES with a guard test — it is what the staff page and export CSV are scoped to, and forgetting it would have written every signup row correctly and shipped none of them
- PROTECTING_SOURCES extracted (the list was inlined in both field_protected? and preload_protections) so a signup value blocks the next BruinQuest overwrite on both paths
- A dedup guard: same buid + source table + field + value with a non-skipped status suppresses, because the profile hub makes re-saving a section routine
- The logger swallows its own exceptions — it runs inside a public save and must never cost an alum their submission
- crm_data_changes.champion_signup_id FK, mirroring cp_champion_id
Diff signup-reported employment against the Phase 24 employments table, and student orgs against alumni_affinities.
Features:
- Employer match is checked against EVERY employments row, not just current_employment — employments is one-to-many, and checking only the most recent would misflag an alum's second job as a new employer
- No match diffs against current_employment; an alum with zero rows logs a blank old_value
- Reported job_title and industry ride as notes on the employment row — a title diff with no employer context isn't actionable
- Student orgs: additions only. A group missing from a form is silence, not a departure
- affinity_other logs under its own field name with a staff-review note, never as a code
- Resolves two Phase 1.4 backlog items
Surface unlinked signups as 'needs identity match'; linking one retroactively runs the CRM logger so the held update isn't lost.
Features:
- needs_identity_match is narrower than 'not linked' — unlinked AND actually holding data the export is waiting on, with a test asserting its field list matches the logger's coverage
- Dashboard callout, list filter and row badge in Signup Admin, plus a pre-link explainer on the signup detail page
- assign_alumni runs the logger retroactively and reports how many held changes were queued
- Csv::CrmDataChangeExporter extracted from the controller per the CSV Export Checklist — the export was inline with an Alumni.find_by per row
- Two new CSV columns: Source Table (field_name alone stopped identifying the target once employer_name and affinity_code existed) and Champion Signup ID
- 50 tests across the logger, exporter, models and three controllers
Phase 24: Alumni Employment Data ✓ Complete
Staff could not answer 'which of our alumni work at HCA?' — a question that comes up constantly for corporate partnerships, event hosting and career-connection asks. Advancement Services has had the CRM employment export the whole time; it had never been ingested. Adds an employments table, its import, profile display, and a fuzzy employer search. Lookup Portal only. Sequenced BEFORE phases 22 and 23 despite the higher number: phase 22.4 is written against a 'alumni has no employment data to diff against' premise this phase invalidates.
View Documentationemployments + employment_import_batches tables and their models. Nothing routed or reachable.
Features:
- employments: current employment only, one-to-many — the feed has no history, so a history table would be fiction
- Unique source_employment_id (BruinQuest record id) as the upsert key; FK employments.buid → alumni.buid
- GIN trigram index on employer_name_normalized — new pattern here, justified by search being trigram-first
- Employment#normalize_employer keeps legal suffixes (LLC/Inc) — stripping them would collide distinct entities
- employer_name is nullable: blanks are a large export bucket and the title/date are still worth keeping
- Alumni has_many :employments (dependent: :destroy, required by the FK) + current_employment
- 42 model tests
Csv::EmploymentImporter — the parse, gap classification, and the per-BUID replace that keeps employment current instead of accumulating.
Features:
- Per-BUID replace: the file is the complete current set for every alum it names usably; alumni absent from it are untouched
- A BUID whose every row was rejected is treated as saying nothing — no deletions on the strength of a broken row
- Deletions travel as synthesized action: :delete rows, so the preview lists them, not just counts them
- Blocking gaps: blank_buid, missing_alumni, blank_source_employment_id. Reported-but-imported: unparseable_start_date, unknown_started_qualifier. blank_employer imports silently (warned in preview, not a gap) — it's the largest blank bucket in the export and there's nothing to chase upstream
- ~3,850 blank padding rows per export dropped at parse time, excluded from every count
- Csv::DateParser reuse — verified 4/1/84 → 1984, 7/27/26 → 2026 against the real sample
- 27 service tests incl. re-import no-op and shrinking-set deletion
Settings → Data Imports → Employment (CRM): the same background scan → preview → commit flow as the 18.3 education imports, with the delete count made impossible to miss.
Features:
- 7 routes + 7 Settings::AlumniController actions; EmploymentImportScanJob + EmploymentImportApplyJob
- Red banner + a dedicated 'Records to be removed' table above the incoming rows
- deleted_count threaded through the batch, the status JSON and the summary line
- Stale batches (>30 min in scanning/running) auto-fail on poll — catches R14/R15 OOM kills
- Gap CSV carries BUID + BQID; link opts out of Turbo per the CSV Export Checklist
- 42 controller + job tests
Employment card on alumni#show, after Degrees — all current rows, most recent first.
Features:
- Start date rendered with its qualifier: 'Since Aug 2013' vs 'Since on or before Mar 2026'; undated rows show no start line
- Blank employers render as 'Employer not on file' — the title and start date are why the row was kept
- Empty state consistent with the other cards; controller preloads :employments
- Provenance note: import-sourced, not editable in the app
The point of the whole phase: find alumni by where they work, tolerant of partial input and misspelling.
Features:
- filter_by_employer (ILIKE) → filter_by_fuzzy_employer (trigram) with the fuzzy_applied flag pattern already used for name
- Both match through a subquery, not a join — employment is one-to-many, and a join would list an alum once per matching position
- Search terms run through the same normalization the importer applied, so 'Prince Properties, LLC' typed verbatim matches
- No soundex — it is tuned for single surnames and produces garbage on multi-word company names
- Employer input in Advanced Search Options, search chip, result-row title + employer under the name (LinkedIn-style)
- Csv::AlumniExporter gains employer + job_title (most recent only)
- 27 tests across the model, filter service, controller and exporter
Phase 25: Signup Data in the Lookup Portal ✓ Complete
The v2 signup flow had been collecting location, interest leaning, student groups and return-visit activity since Phase 19, and none of it reached the staff who use it. The profile showed a v1-shaped card describing a wizard that no longer exists, district search matched CRM addresses only, and the CSV had one 'champion' column. Adds a single read layer (Alumni::SignupProfile) and routes the profile, search, filters and export through it. Lookup Portal only — reads from the signup ecosystem, writes nothing to it.
View DocumentationAlumni::SignupProfile — one canonical signup per alum and every derived value read from it. Nothing routed or visible.
Features:
- Alumni#current_champion_signup: newest active signup, memoized, read through the loaded association
- Effective location = signup ZIP over alumni.zip; alumni.zip is never written, because a signup value is pending truth the CRM catches up to
- Location#source is :signup only when the ZIP is driving AND differs — converged values carry no badge
- Affinities split into crm_affinities / self_reported_affinities, deduped: nothing BruinQuest confirms is repeated as self-reported
- stored_interest_spectrum / stored_top_interest_area — the no-recompute variants for list views
- No scorable quiz and no chosen role means nil, not a fabricated lean; an all-zero spectrum counts as nil too
- Index on champion_signups (buid, created_at) backing the effective-ZIP subquery
- Alumni#reload clears both memos — reload drops association caches but not instance variables
- 23 service tests
alumni#show finally shows what v2 collected: effective location, interest leaning, self-reported groups, signup history.
Features:
- Header location shows the effective value, with a self-reported badge and the CRM address beneath it only on divergence
- Champion Role replaced by interest leaning — seal art reused (area keys == role keys), plus the four-bar spectrum partial shared with the public quiz reveal and Signup Admin
- Affinity card badges CRM-confirmed vs. self-reported and renders the free-text affinity_other
- History high points: signup date + v1/v2, Returned / Added info stamps, or an explicit 'has not returned'
- 'Signup In Progress' relabeled 'Started the form, never finished' — v2 cannot produce that state
- Controller preloads champion_signups: :district
District and affinity search stop being blind to what alumni told us.
Features:
- Alumni::EFFECTIVE_ZIP_SQL — correlated subquery, not a join, so several signups can't multiply result rows
- filter_by_district matches the effective ZIP: someone who moved appears in their new district and not their old one
- filter_by_affinities matches alumni_affinities OR signup affinity_codes, via two subqueries
- Result row: effective district with a dot on self-reported, leaning seal, source-badged affinity chips (shared _affinity_chips partial)
- Fixed: the affinity dropdown's server-rendered options carried Affinity#id against a scope matching affinity_code
- filter_for_search preloads champion_signups and alumni_affinities
The export carries what the alum said, beside — not instead of — what the CRM holds.
Features:
- signup_city / signup_state / signup_zip / signup_district / location_source
- top_interest_area + all four area percentages, driven by a frozen INTEREST_AREAS order so header and row can't drift
- signup_affinities + signup_affinity_other; the existing affinities column stays CRM-only
- Stored scores only — recomputing a v1 quiz 49,000 times for a decorative column isn't a trade worth making
- A width test asserts every row matches HEADER
Six options describing v1's five-step wizard become one question: did they fill out the form?
Features:
- all / completed_form / no_form — v2 requires a ZIP on step one, so a signup is complete on creation and four old options could never match anything new
- prospect_status split out to its own 'Manually flagged prospect' checkbox — a staff judgment, not a degree of completion
- ALUMNI_FILTER_ALIASES maps retired values forward so bookmarks survive and label themselves with what they now do
- no_form handles a NULL buid explicitly — NOT IN evaluates to NULL and would have dropped them
- 'Has Alumni Network Account' left untouched; the two filters must not bleed together
Phase 23: Namespace & Naming Unwind ✓ Complete
Five apps share one codebase and none of the namespaces reliably said which app a file belonged to. /champions/ (Alumni Network Admin) and /champion_signups/ (Signup Admin) sat one character apart and administered different products; the signup ecosystem lived under public/, which describes auth state rather than audience. Documentation had already been tried and failed — CLAUDE.md described the boundary in a table, in bold, and as a numbered rule, and it was violated often enough that two of the five apps had to be put on hold. The mechanism is why: an agent deciding where a file belongs reasons over grep output, and grep -rn champion returned both directories with nothing marking one as frozen. Structure in a path is present at every decision point; documentation is present only if it was loaded. So the phase prefers enforcement over description and structure over both. Scope was cut twice against measured churn: the lookup/ namespace (~927 refs, zero misroutes prevented) and all of 23.5 (Cp:: → An::, 389 files, the only migration).
View DocumentationA rename removes today's collision; a test prevents tomorrow's. The cheapest item in the phase and the only one that enforces rather than describes.
Features:
- test/architecture_test.rb — held trees gain no files, cross-surface Cp:: references never grow, no controller claims another surface's layout
- A ratchet with a committed, dated baseline, not a clean assertion — the tree does not pass a strict version, and a test that fails on arrival gets disabled within a week
- Comments excluded from reference counts, which cut the apparent Cp:: inventory from 34 files to 17: half the 'coupling' in this repo is documentation about coupling
- 'Every controller declares a layout' is not assertable (layouts inherit); the testable inverse is that no controller claims a layout belonging to a surface it does not live in
- Cp::Champion::INDUSTRIES extracted to shared Industry::ALL — the live alum-facing signup flow was importing a constant from the frozen portal, which any later rename would have taken down
- Verified by adding a scratch controller to a held directory and confirming both assertions fired
- 7 tests / 107 assertions
Split the signup ecosystem by audience, not auth state. public/ described the wrong axis — signed-in returning-visitor pages lived there too.
Features:
- public/ → signup/ (alum-facing), champion_signups/ → signup_admin/ (staff); layouts/public → layouts/signup
- Alum-facing URLs deliberately unchanged — /sign-up, /profile/:code and /opportunities/:slug are in alumni inboxes already
- 79 files and ~436 route-helper references, against an estimate of ~15 — the estimate counted files to move and missed the references entirely
- The real hazard was not in the plan: StaffNotification#url PERSISTS a route path, so eight permanent 301s were needed or the entire existing staff notification inbox would 404 on click
- Redirects ordered specific-first so champion_signups/:id cannot swallow champion_signups/opportunities as id='opportunities'
- Every redirect explicitly named legacy_* — unnamed, Rails derives helper names from the path and silently recreates the names the rename removed, except now they 301 instead of erroring
- Models stay put: 'Champion Signup' is the product's own correct name
Corrects a false premise in the phase spec — that every file belongs to exactly one app. It is false for the core domain objects, and the walls were causing the misroutes.
Features:
- Champion interest seals/icons/spectrum → app/views/shared/champion_interest/ — the artwork lived in the frozen champions/ tree while three other apps rendered it, so editing a deliberate cross-app design choice was a held-surface write
- Affinity and Industry own their vocabularies; ChampionSignup did not validate industry at all while Cp::Champion did — same taxonomy, two apps, two levels of enforcement, no loud failure
- IdentityField map + read-side translation, fixing the pref_name/maiden_name defect
- Roadmap controller out of champions/ to root level, which DELETED CLAUDE.md's hand-carved rule-5 exception rather than porting it, and left 23.4 with no process-critical file
- Cross-surface render assertion added with an empty baseline — 23.3a removed the violations rather than allowlisting them
- include_all_helpers stays ON by decision: global helper availability is how a calculation written for one app benefits the rest
- Four held baselines lowered, in the same commit as the removals
The collision the hold gate exists for. Renames the Alumni Network Admin after the product it administers. Held surface — explicit approval granted for this named scope.
Features:
- 185 files. 20 controllers, 75 views, 3 helpers, 20 test files; 1,401 route-helper references across 154 files. No migration, no schema change
- The rename was GENERATED from bin/rails routes, not grepped — four helper families contain 'champion' and are not this namespace (cp_champion_* Devise, cp_champions_landing, alumnichampions_root, champion_signups), and one helper's own action is named autocomplete_champions
- resources :champions → :members and both 'champion' layouts renamed, pulled forward from the cut 23.5 — the URL is what staff and agents reason over, so /alumni_network/members ships even though the model is still Cp::Champion
- Cp::Champion::INDUSTRIES deleted; the 23.1 drift test replaced with an assertion that no private copy can come back
- StaffNotification#url again, and worse: Cp::NotifyAdminsJob persists a route path on EVERY member signup. Mitigated with a permanent wildcard 301 rather than an enumeration — two controllers and two mailers write /champions/ URLs too, and an enumerated list missed all four
- Query string preserved through the redirect: dropping ?needs_leader=true lands staff on an unfiltered list that looks like a working page while answering a different question
- Silent regression found and fixed: the admin sidebar compares request.path against 25 hard-coded literals, so the helper rename left it highlighting nothing on every page. A route-table-driven rename only fixes what the route table knows about
- Same failure in ActionItemsService's six navbar-bell links. Both pinned by tests, and the sidebar test verified by sabotage
- app/helpers/champions_helper.rb was sitting at root level — nominally Alumni Lookup — with all six consumers in the admin surface
- 34 'Champion Program' UI labels → 'Alumni Network'
CUT July 30, 2026 before any work began. 389 files, 4,413 references, 56 tables, and the phase's only migration — bought a consistency benefit on a frozen surface and prevented zero misroutes.
Features:
- The phase's own value-vs-churn ranking already answered 'prevents misrouting? No' — Cp:: is a fossil, but an unambiguous one: nothing else looks like it, so nothing has ever been misfiled into it
- The counter-argument was only about timing ('on-hold is the only zero-conflict window that will ever exist'), which does not buy the largest file count and the only irreversible step in the phase
- Its migration was the one change in Phase 23 that fails SILENTLY — a missed polymorphic type string makes associations return nothing rather than error
- 23.4 absorbed the three consequences that mattered: resources :members, both layout renames, and the INDUSTRIES collapse
- Cost accepted: the portal keeps Cp::/cp_*, and Phase 26.4 loses the forcing moment that would have surfaced cp_profile_changes vs crm_data_changes
- Filed to BACKLOG on the same terms as the dropped lookup/ namespace — revisit only with the monolith split
The documentation half of the refactor-vs-document question. Not an alternative to 23.1–23.4 — it is what makes them navigable, and it has to record a cut, not just renames.
Features:
- CLAUDE.md rules 3–3b INVERTED: 'a champion-named path is a held path' was true until 23.4 and is now exactly backwards — no champion-named directory remains in either held tree, while ChampionSignup (the live product's own name) is everywhere
- Canonical Surface Inventory promoted into CLAUDE.md — the artifact that answers 'what does in the lookup mean?'
- Cp:: documented as PERMANENT, not transitional; every doc anticipating An:: corrected
- BACKLOG 2.1 (a consolidated admin.alumnilookup.com) reconciled against Decision 1, which rejected a generic /admin/ — two products have staff admins, so an admin path must name the product it administers
- The 23.4 findings routed into the debug and patterns skills: string literals are invisible to a route-helper rename, and StaffNotification#url has now bitten two sub-phases running
- Doc link policy decided rather than fixed ad hoc: current-state docs get corrected, historical phase specs keep their prose because rewriting them falsifies the record
Phase 27: Returning Alum Identity ✓ Complete
An alum who signed up six months ago comes back. How do we know it's them, how much do we trust that, and what do we let them do about it? The app already did passwordless authentication — session cookie, signed profile token, permanent access code, 'email me a link' — it just wasn't called that, wasn't durable, and had no entry point: layouts/signup rendered a header with no navigation, so 'sign in' was reachable only from a page you couldn't reach unless you were already in. The phase names the mechanism that already existed, makes it durable, and stops one person from becoming three rows. Passwordless permanently (Decision A): the data is name, grad year, contact info and interests, so a password would buy reset flows, breach surface and support load in exchange for a credential people forget on a conversion funnel. Build order was inverted on August 6 — 27.2 ships before 27.1, because 27.1's inline email check is an enumeration oracle the phase commits to shipping throttled, and every throttle it depends on lived in 27.2. All eight sub-phases complete as of August 12, 2026. 27.7 reversed Decision B outright — the recognized tier is gone, a remembered device signs you in, and a six-digit code rides with every magic link so a cross-device return lands you back where you were. 27.8 was then written against the shipped flow rather than the spec, and found that three of its four items were the same defect: a second copy of a decision, each drifted within one sub-phase of being created.
View DocumentationShips first. A front door for a mechanism that already worked, plus the metering the rest of the phase depends on.
Features:
- GET /sign-in — a dedicated page wrapping the existing 'email me a link' form, which until now was reachable only by scrolling the landing page; the landing form stays, both render one partial
- Enumeration-neutral by contract: known and unknown addresses get an identical response, so the form can't be used to probe who has signed up
- Clicking an emailed profile link now signs you in for real — #show was the only entry point that never adopted the session, so a magic-link visitor went anonymous the moment they left the hub
- Header shows first name plus 'Sign out' when signed in; first name only, because 27.3's recognized tier shares the partial
- Signing out lands on the landing page with a confirmation, not on a blank signup form nobody asked for
- Three portal throttles renamed portal-* rather than removed — they guard /login and /signup, which are live Devise routes for the frozen portal, not dead code; repointing them would have stripped rate limiting off a production login
- Five new throttles for the live endpoints, closing the /profile-link email-bomb vector that was live in production
- Signup-create throttles scoped to the tokenless who_you_are submit, not the path — POST /sign-up serves all 13 funnel steps, so a path-scoped rule would 429 the first real completion
- Duplicate-nudge email resolves Phase 27 open question 3: the send goes only to the address the submitter typed, so it can't be aimed at a third party
Prevent what can be prevented before the submit, resolve the rest staff-side, and stop a merge from silently killing the link that got them there. No create-time merge — the prevention happens before, the resolution after.
Features:
- Email leads step 1 and an inline check fires on blur — the order is the feature, since a nudge only spares a returning alum the form if it arrives before they fill it in
- POST /sign-up/email-check answers {known: true|false} and nothing else: no name, no masked email, no id
- An account-enumeration oracle, accepted deliberately (Decision D) — what leaks is program affiliation, not a credential, so the lever is cost per query: honeypot gate, 10/hr/IP, fail2ban escalation, disclosure in /privacy
- The check drops the 3-second form clock (min_seconds: 0) and keeps both real traps — blur fires seconds after render, so the timer would have eaten a fast typist's first check
- Nothing blocks: ignoring the notice still creates the row, with the 27.2 nudge email as the backstop
- merge_duplicates_for_email reaches the population the BUID key structurally couldn't — BUID assignment is manual, so a signup with no matching Alumni row could never be merged at all
- A set whose rows carry different BUIDs is refused, not warned about — a shared family inbox looks identical to one person returning, and no bulk email merge exists
- The merger now fills a blank buid from its sources; invisible under the BUID key, but under the email key it would have soft-deleted the only row linked to an Alumni record
- duplicates_by_email fixed before it drove any UI — it grouped on raw email with no blank exclusion, so every blank-email row would have collapsed into one enormous false set
- merged_into_id: access code, profile token and device session all follow a merge to the surviving row, so a mail-merge link sent six months ago keeps working
- Resolution is always one hop — a later merge re-points earlier sources, so there is no chain to walk and no cycle to guard
- A row soft-deleted by deletion still resolves to nothing; the fix must not resurrect deliberately removed records
Upgrades the 2-week session 27.2 shipped into a 6-month remembered device, with the recognized/verified split that makes a long-lived cookie safe on a shared machine. Shipped as one release with 27.6, because the cookie ships with its disclosure or not at all — and that coupling is now asserted in tests rather than left to a launch checklist.
Features:
- ✅ champion_signup_sessions plus a rotating token cookie, rotated on each verified sign-in
- ✅ Recognized tier renders first name and a masked email, permits no edits, cannot open the hub, and always carries a visible 'Not you?'; verified is the existing check, unchanged
- ✅ 'Keep me signed in on this device for 6 months', unchecked by default — that checkbox is the consent (Decision E), and there is no cookie banner
- ✅ Signup::Identity owns the anonymous/recognized/verified decision for the whole surface, resolving explicit credentials ahead of ambient ones and delegating session resolution to the one place that already owned it
- ✅ token_digest, not token — a spec deviation. This is the longest-lived credential the surface issues, so a raw-token column would make a database dump six months of access to every remembered device
- ✅ The consent rides in the emailed link, not the requesting browser's session: people tick the box on a laptop and open the email on a phone, and the device that opens the link is the one in use
- ✅ Remembered devices follow a merge — the fourth credential to route through 27.1's follow_merge, after the access code, the profile token and the session
- ✅ Device revocation: 'Not you?' and 'Sign out' destroy the row, not just the cookie, and leave other devices alone
- ⏳ The device LIST stays in 27.5; device_label and revocation exist to support it
Brings the recognized tier to the form that collects the most contact data. Half of the original scope turned out to already exist — 21.6 shipped the anonymous responder's account offer — so what was actually missing was that the opportunity controller had never adopted 27.3's tier model. It still carried the private current_signup it was written with in 21.2, resolving only verified credentials, so the alum the header greeted by name was an anonymous stranger one click later.
Features:
- ✅ Decision B's self-contradiction resolved in favor of the restriction: its tier table grants recognized a 'prefilled contact block' and the paragraph beneath forbids 'PII beyond first name and a masked email'. The collapsed block renders a MASKED SUMMARY — the real email, last name, phone and ZIP never reach the page in any form, and the server fills them at submit
- ✅ The rule is enforced, not documented: integration tests assert the real values are absent from the response body, so a later change that 'helpfully' adds a hidden field fails the build
- ✅ A second identity authority removed — the controller's own current_signup used a bare find_by(id:) with no follow_merge, so a session pointing at a merged-away row went silently anonymous here while the hub resolved it correctly
- ✅ A recognized response links as an email match, NOT as verified — that flag tells staff a response was identity-proven, and a device cookie is Decision B's explicitly weaker claim
- ✅ No profile token is minted for a recognized submitter. The redirect previously carried one unconditionally, which would have turned a 6-month cookie into hub access — the sharpest edge in the sub-phase
- ✅ Thank-you page gains a third branch: greeted by name, offered sign-in, given no hub link. Greeting does not weaken 21.2's enumeration-neutrality contract, which guards against someone TYPING an address they don't own
- ✅ 'Not you?' returns to the same opportunity rather than a signup form — resolved from a slug, never a return path, since reset is a DELETE anyone can aim at a page built to be shared
- ⚠️ Found in the receipt: the thank-you page rendered checkbox answers raw, so a two-box answer read 'FridaySaturday' and an empty one printed a blank row. It was the fifth consumer of the semantics OpportunityResponse's own comment says four consumers need. Fixed during extraction rather than copied into a second consumer
What a signed-in alum can see and do about their own record. Sign out already shipped in 27.2/27.3, so this is the responses list and the device list — both built entirely on what 27.3 already put in place.
Features:
- ✅ 'Things you've said yes to' — collapsed rows expanding to the full receipt, because a title alone cannot answer the question an alum actually has, and a link back to the form answers a different one
- ✅ The thank-you page's receipt became a partial with two consumers on the day it moved — the shared layer as first answer, not as a later extraction
- ✂️ The device LIST was built and then cut — sign out already destroys the device row, so the only action an alum actually needs was covered before the list existed. What it added was managing a device you are not currently using, which presumes an alum both knows they left one remembered and thinks to look. ChampionSignupSession and device_label stay; only the management UI is gone
- ✅ Open question 4 closed: a recognized alum does not see past responses, and the degradation it asked for already existed — the hub is verified-only and redirects to a working sign-in page. Adding a recognized-tier branch would have been dead code guarding a route recognized visitors cannot reach
Reverses Decision B. The recognized tier — a 6-month cookie deliberately weaker than a session, granting a first name and a masked email but never the hub — was the single largest source of complexity on this surface, and its own premise did not survive the device-list cut: that feature was dropped because 'they can just sign out', which is exactly the mitigation the tier existed to avoid depending on. Passwordless was never the cost and does not change (Decision A) — a password would add a credential people forget on a conversion funnel plus a reset flow, and a password reset flow IS a magic link, so the mechanism would stay and a second one would join it.
Features:
- ✅ Two states, not three: signed in or anonymous. A remembered device now signs you in outright, and is adopted into the session on the request that resolves it
- ✅ Deleted with the tier: the masked-email helper, the recognized banner, the masked contact block, the three-state opportunity form, the third thank-you branch, and the receipt's masking local
- ✅ THE TRADE, STATED PLAINLY: a shared computer where someone ticked the box now exposes a full profile for six months. Sign out is the mitigation, and it destroys the device row rather than only the cookie
- ✅ A six-digit code rides in the SAME sign-in email as the link — no second send, no new spend. Cost on Heroku is zero; it uses the Mailgun path already there. SMS was rejected: Twilio is a paid add-on plus A2P 10DLC registration, and phone is optional in the data
- ✅ The code exists because a magic link structurally cannot return you to where you were — request on a laptop, open the email on a phone, and the link signs you in on the phone. A code never leaves the browser you were already in
- ✅ Five wrong attempts kill a code, and the sixth fails even if correct. Six digits is a 1,000,000-key space, small enough that a rate limit only slows a walk through it; the per-code cap is the bound that matters
- ✅ Return-to-where-you-were rides in the SESSION, never the URL or the email — so it cannot be aimed at anyone, and the code path is guaranteed to still have it. Refuses //evil.example.com, absolute URLs and schemes
- ⚠️ Found: the hub guard read the session directly, so it ran BEFORE the cookie was adopted — 27.7 would have shipped with the header greeting someone by name while the hub bounced them to /sign-in. Caught because a 27.3 test that was supposed to invert kept passing
- ⚠️ config/session_store.rb was deliberately NOT touched — its 2-week expiry is global and governs staff Devise sessions, so raising it would have extended staff access to 49K alumni records to fix an alum-facing convenience
- ✅ Both policies rewritten and PRIVACY_POLICY_VERSION bumped to 2026-08-12. The compliance ratchet asserted 'being remembered is not being signed in', which became false — the assertion inverted rather than being deleted
Written against the shipped 27.7 flow rather than against a spec — somebody walked it and found four things. Three of the four turned out to be the same defect wearing different clothes: a SECOND COPY of a decision, each of which had drifted within one sub-phase of being created. A second sign-in form (27.2 put one on the landing page and 21.6 inlined a third on the thank-you page; 27.7 then added the code box and the consent checkbox to only one of them, so submitting either copy produced an email whose main instruction pointed at a page with nowhere to follow it). A second call to action (the landing page kept selling 'Get Started' under a 'Welcome back, Chip' banner — and that button carries no signup_token, which handle_who_you_are reads as 'creating', so following it opened a second signup under the same address). A second reading of 'home' (the header logo and sessions#create each decided independently). The fourth was a comment documenting a second copy, which leaked onto a public page. The rule that would have prevented all four is already in CLAUDE.md §7; what this adds is the operational form — when a page needs a control another page already has, LINK to that page, do not copy the control. Two of the three fixes were simply deleting the copy.
Features:
- ✅ One sign-in page. The landing and thank-you copies of 'email me a link' are links to /sign-in now — deleted, not shared, because a link was always the right answer. The thank-you link carries return_to so signing in from a receipt returns to that receipt
- ✅ Removed with them: the entry_point param and hidden field, signin_entry_point?, the two-branch link_sent_path, sessions#profile_path_for, and the landing page's link_sent / link_request banners. link_requested keeps its meaning for historical rows but has no writer — asserted, so a third form cannot quietly reintroduce one
- ✅ The typed address rides back into the code box, so the only thing left to do is type six digits. It is echoed from their own input, and it rides the HONEYPOT branch too — a redirect that differs between a trapped and a real submission is the tell the trap exists
- ✅ The code now LEADS the email and the link is the fallback. The link opens the profile hub on whatever device reads the mail, abandoning whatever you were doing; the code returns you to the browser you left, with its stored return_to. Pinned as an ORDER in both parts — a test asserting both are present passes on either arrangement
- ✅ The landing page has an education mode: same explanation of what a Champion is and the four interest areas, every CTA pointing at the profile. One page with one flag, not two templates — the middle of the page is genuinely identical, and splitting it would reproduce the exact drift being fixed
- ✅ signup_home_path is the single definition of 'home': the profile when signed in, the landing page otherwise. The header logo uses it with no branch in the markup
- ⚠️ Found: four lines of developer commentary rendered as page copy on an alum's own profile hub and on the public thank-you page. An ERB comment ends at its FIRST %>, so the escaped example tag inside one closed it early — the <%% escape protects the output delimiter, not the scanner. Valid HTML, 200 response, no warning; the existing test asserted the answers were present, and they were — the leak rendered above them
- ✅ erb_comment_integrity_test.rb now scans every view on every build. Held trees are excluded BY PATH rather than baselined, because one real instance sits behind that exclusion (cp/profile/_completion_banner leaks its own header onto the portal's profile edit page) — named as debt, not laundered into a clean baseline
- ✅ Two things verified by sabotage rather than trusted: the ERB detector (empty HELD_TREES → reports the known cp/ instance) and education mode (force it false → 3 of 4 tests fail; the fourth asserts the educational content, which holds in both modes)
Shipped in the same release as 27.3. The March cookie policy said 'that's it — one cookie'; this adds a second, and corrects two other statements that had quietly become untrue.
Features:
- ✅ Privacy policy revised and version bumped to 2026-08-08: the second cookie and its duration, that being recognized is not being signed in, the no-sale/no-share statement for GPC, and 27.1's enumeration trade-off — disclosed rather than left sitting silently under the policy's no-tracking claims
- ✅ Cookie policy revision: point-of-use consent replaces the old 'everything is strictly necessary' rationale for having no banner
- ✅ Montserrat self-hosted — 10 woff2 subsets with their unicode-ranges preserved, so a Latin-only visitor still downloads two files. The alum-facing surface now makes zero requests to Google
- ✅ Retention rule shipped WITH the table, not retrofitted: champion_signup_sessions:prune, plus a stats task to answer whether it is running
- ✅ A deletion destroys sessions rather than stamping deleted_at — those rows hold a user agent and an IP, and follow_merge returning nil stops the access, not the retention. A merge deliberately does the opposite and keeps them
- ⚠️ Found and disclosed rather than fixed: /sign-up eagerly fetches five JavaScript modules from cdn.jsdelivr.net via modulepreload, so 'no third-party content' was false when first written. Both policies now name jsDelivr, and the origin baseline is pinned as a ratchet. Vendoring is backlogged with CSP enforcement, which phase-27 §9 explicitly scoped out
Phase 28: Google Sign-In and One Row Per Person ⚡ In Progress
Phase 27 gave a returning alum a way back in and it costs them a round trip through their email every time. Google collapses that to two clicks — but a verified credential has to resolve to exactly one record, and champion_signups had no unique index on email, so find_active_by_email resolved collisions by taking the newest row. A fair guess under a mailed link, which only ever reaches one inbox; not a fair basis for signing somebody in. So uniqueness lands first and the guess is retired rather than hardened. Decision A: ChampionSignup becomes account-LIKE, not an account — merging it into Cp::Champion was proposed and rejected on August 14, because it would make the live intake product depend on the frozen one, because 30% of signups are mid-funnel and a Devise account cannot be half-created, and because BUID is already the shared key at 95% coverage. Decision B: Google never CREATES a signup, it matches an existing one or hands the person to the form with verified details prefilled — creating on miss is a duplicate generator aimed at the one maintenance task staff already do by hand. Decision C: one callback, two labels, so the destination is decided by what is found and never by which button was pressed.
View DocumentationShips first — auto-linking a verified identity is unsound while an address can name two rows. A partial unique index on LOWER(email) WHERE deleted_at IS NULL, matching scope :active exactly.
Features:
- Partial unique index on LOWER(email), not the raw column — find_active_by_email compares lowercased, so an index on `email` would report success while the finder still saw two rows
- Scoped to active rows: all 15 historical duplicate groups live in soft-deleted rows and stay readable, and a staff removal returns the address to circulation
- The migration REFUSES to run on an unresolved collision and names the addresses — a merge destroys a record and belongs to a staff member, with the signup_merged event every other merge writes
- find_active_by_email loses its newest-row-wins tie-breaker and now reads through active_by_email instead of hand-rolling the LOWER clause it used to duplicate
- ✅ The 27.1 duplicate nudge moved to the FAILURE path rather than disappearing — it sat inside `if save`, which the index now makes unreachable. Gated on the address being the only thing wrong, so an incomplete submission still sends nothing (27.2's property)
- ✅ The whole email-keyed duplicate subsystem was deleted — scope, two predicates, the merger's email key, ConflictingBuidsError, the admin action, route, banner, badges and buttons. The tests forced it: every one described a state the database can no longer hold
- BUID stays the only merge key and is now the only duplicate shape the app can produce — two addresses on one BUID, ordinary at 95% coverage
- Sabotage-verified, and it caught a tautology: every case-insensitivity test ran through the validation and would have passed against an index on the raw column. One test pins LOWER() in the index, via update_column. A second tautology hid in the sabotage itself — parallel workers rebuild their databases from schema.rb, so PARALLEL_WORKERS=1 was required to make it real
The payload. One callback at /auth/signup_google/callback — a second provider name, because the portal's callback already claims /auth/google_oauth2/callback on the signup domain and narrowing its constraint would be a held-surface write.
Features:
- Four-way resolver: google_uid, then verified email, then BUID via AlumniLookupService, then hand off to the signup form
- email_verified is a hard gate — neither Cp::Champion.from_omniauth nor User.from_google_oauth checks it, and an unverified address that auto-links is account takeover on a public funnel
- The BUID fallback rescues the alum who signed up with @belmont.edu and authenticates with a personal Gmail; the stored email is never overwritten, because Advancement Services may already have it
- Connected Accounts in the hub — unlink is unconditionally safe here because passwordless is permanent, so Google can never be someone's only credential
- PRIVACY_POLICY_VERSION bump to 2026-08-14: auto-linking without an explicit connect action, and identity resolving through an address the person did not present
- ✅ §3.4's refusal turned out to be TWO shapes, not one. The spec's case — the uid on one row while the email names another — is genuinely unreachable after 28.1. The reachable one arrives through the BUID fallback: link a personal Gmail, then authenticate with a second Google account whose address is also on the alumni record
- ✅ And refusing the reachable one was wrong, corrected the same day on a real two-account report. Signing in and moving the link are separate decisions: outcome 3 already signs that person in when NO link exists, so refusing the identical credential because one does treats the link as a security boundary it isn't. It now signs in, leaves the link alone, and says so — §3.3's stored-email rule applied to the second column
- ✅ layouts/signup rendered no flash at all — the only layout in the app that didn't, because nothing on this surface had ever set one. A redirect-only OAuth controller has no page of its own, so the refusal reached the user as a silent bounce to /sign-in
- ✅ Both lookups run even when the uid already matched. A short-circuit would have made the first branch dead code that still read as protection — and the test proved it, failing until the resolver stopped short-circuiting
- ✅ Server-side email enforcement pulled forward from 28.3. The handoff without it would make "prefilled from Google" a suggestion a crafted POST could ignore; 28.3 still owns the visual lock and the step-1 restructure
- ✅ /auth/failure is shared by both providers and redirected to the Alumni Network's login page. Captured for signup_google only, constrained on the strategy param OmniAuth appends, so the portal's failures route exactly as before — verified by asserting the negative
- ✅ The button hides itself when GOOGLE_CLIENT_ID is absent. Without the provider there is no middleware at /auth/signup_google, and a control that posts into a 404 reads as a broken site rather than a feature that is off
- Sabotage-verified twice: forcing email_verified true breaks 4 tests, and dropping the host from route recognition breaks the collision assertions that justify the second provider name
Step 1 shortens to identity-only, which also repairs a live defect: ZIP has been a step-1 field since about November 2025, and calculate_status returns 5 ("Completed") the moment ZIP is present. Five active rows reported Completed having done only step 1.
Features:
- Step 1 becomes Google + email + first + last + graduation year; ZIP, phone, SMS consent and maiden name move to "Where you are now"
- For a Google user, graduation year is the only field typed at step 1
- Locked prefill on the Google-miss path — the address was verified moments ago, and letting someone retype it is letting them type their way into a duplicate
- The pending Google identity rides in the session, cleared on completion AND on abandonment — a stale one would attach one person's Google account to another person's record
- ✅ The spec's backfill plan could not have worked. calculate_status reads zip_code.present?, and moving the field does not remove ZIPs already stored — while before_save :ensure_status_updated re-derives status on every save, so even a direct UPDATE would have been undone by the next write. The dry run proved it: nothing to change
- ✅ So the repair is to the RULE, not the rows: the top rung now needs a ZIP plus evidence of progress past step 1. Choosing that evidence took three tries — requiring the v2 employment fields would have demoted 131 v1 signups that genuinely finished (v1's wizard ENDED at ZIP), and requiring the rung below still demoted 12. Measuring each against real data before accepting it is what caught both
- ✅ TCPA consent-clearing moved from the step-1 controller to the model. "Clear sms_opt_in when the phone is blank" lived in the handler for the one step that no longer has a phone field, while the step that does had none
- ✅ Writing a comment WARNING about ERB comment tags broke the profile hub — a closing delimiter inside a Ruby comment still closes the block. Every view is now compiled by a test
- ✅ The Google hand-off had no exit. Picking the wrong account left you stuck: the email is locked, and "Get Started" returns to the same prefilled form because the identity is in the session. The form now carries a "Not you? Start over" link and the landing names the pending account — rather than making "Get Started" reset, which would silently drop the link for anyone who came back to read the page and then continued
- Sabotage-verified twice: restoring ZIP to the step-1 permit list and flattening the ladder rule each break the tests written for them
This roadmap is only visible on staging and development environments.
💡 Feature Backlog
Ideas we're considering for future development. Priority and timing TBD based on Champion feedback and engagement data.
Alumni Business Directory
HighChampions list their businesses for 'hire a Bruin' discovery.
Native Mobile App
HighiOS/Android apps with push notifications.
Newsletter Tool
MediumStaff-curated newsletters with fixed + community content sections.
Real-Time Messaging
MediumWebSocket-based instant messaging with typing indicators.
Map View
MediumVisual map of Champions across the country with clustering.
Advanced Search
MediumCombined filters, saved searches, 'Champions like me' recommendations.
Visit Mode
LowTemporarily view another community's content without changing primary.
Have an idea? Share it with the Engagement Team!