alumni_lookup

Phase 11: Champion Role Dashboard Card

Canonical sources: Portal philosophy, posture, and language live in /docs/planning/champion-portal/source/README.md. Use these sources (including CHRIST_CENTERED__IDENTITY_STATEMENT.md) when writing specs or user-facing copy. Prefer quoting/paraphrasing over inventing new language.

Status: ✅ Complete Estimated Effort: 3–4 weeks Prerequisites: Phase 9 Complete (Onboarding & Seeded Questions architecture), Profile Wizard Role Quiz functional

Related Documents:


Sub-Phase Progress

Sub-Phase Name Status
11.1 Data Model & Seed Library ✅ Complete
11.2 Daily Pack Selection Service ✅ Complete
11.3 Dashboard Card UI (States A & B) ✅ Complete
11.4 Roles Landing Page ✅ Complete
11.5 Admin CRUD (Role Ideas) ✅ Complete
11.6 Analytics & Success Metrics ✅ Complete (events only; metrics dashboard deferred)
11.7 Testing & QA ✅ Complete

Table of Contents

  1. Overview
  2. Goals
  3. Non-Goals
  4. Key Design Decisions
  5. Terminology
  6. Data Model
  7. Selection Algorithm
  8. Dashboard Placement & Card States
  9. Roles Landing Page
  10. Admin CRUD
  11. Personalization Rules
  12. Community Leader Variant
  13. Analytics Events
  14. User-Facing Copy
  15. Seed Data Library
  16. Testing Requirements
  17. Definition of Success
  18. Rollout Notes
  19. Documentation Updates

1. Overview

Phase 11 adds a Champion Role card to the Champion Portal dashboard. The card serves two purposes:

  1. Before role selection (State A): Invite Champions to discover their role through the existing quiz — using recognition-first language per ALUMNI_CHAMPIONS__ONBOARDING_PHILOSHOPY.md.
  2. After role selection (State B): Surface a daily pack of 3 role-aligned action ideas that deeplink into portal features, giving Champions a concrete “here’s what you can do today” experience.

Core Thesis

“Champions should always have a clear next action they can take today.” — per ALUMNI_CHAMPIONS__OVERVIEW.md

The role card operationalizes this by providing personalized, rotating suggestions tied to the Champion’s self-identified role. Ideas are concrete (“Post a welcome to a new member in [Community Name]”), not abstract (“Get involved”).

What This Phase Delivers

Feature Description
Role Dashboard Card Right-column card between Events and Communities with two states
Daily Idea Pack 3 role-filtered ideas per day (1 primary + 2 secondary)
Roles Landing Page Standalone /roles page with all four roles explained + quiz CTA
Admin CRUD Portal Admin interface to manage the idea library at /champions/role_ideas
Seed Library 20–25 ideas per role + 15 CL ideas (~115 total) delivered as seed YAML
Analytics 6+ new activity events tracking card engagement

2. Goals

Goal Metric
Increase role adoption 75%+ of Champions have selected a role within 60 days of launch
Provide daily action ideas Every Champion with a role sees a pack of 3 ideas on each visit
Drive portal feature usage Measurable click-through from idea CTAs to target portal features
Engage pre-verified Champions email_verified Champions see State A card with quiz and roles page

3. Non-Goals

What Why Not
Multi-role support Phase 11 is single-role only; primary_role field already exists
Replacing profile wizard role selection Card links to existing quiz; does not duplicate it
Rebuilding the quiz Quiz exists at /profile/wizard/quiz/:question; Phase 11 links to it
Gamification / streaks / points Keep it invitational, not competitive (per verbal style guide)
Push notifications for ideas Ideas surface passively on dashboard visit only

4. Key Design Decisions

Decision Rationale
Persisted daily pack (Option 1) Mirrors existing SeededQuestionExposure pattern; enables robust anti-repeat, analytics, and refresh support
Pack of 3 (1 primary + 2 secondary) Enough variety without overwhelming; primary idea has CTA button, secondaries are lightweight text rows
Champion time_zone for midnight Already auto-assigned from ZIP code via ZipCodeTimezone.lookup; defaults to "Eastern Time (US & Canada)"
Mixed CL pool Community Leaders see ideas from both their Champion role AND a CL-specific pool, blended together
Admin CRUD in Lookup portal Follows seeded questions pattern at /champions/role_ideas; portal_admin access
New /roles landing page No standalone role education page exists; needed for “Learn more” link and as a reusable destination
Seed data in YAML file Follows docs/seeded_questions_100.yml pattern; 5 representative examples in this spec for tone reference

5. Terminology

Term Definition
Role One of four Champion roles: Community Builder, Digital Ambassador, Connection Advisor, Giving Advocate
Role Idea A single actionable suggestion tied to a role, stored in cp_role_ideas
Daily Pack The set of 3 ideas shown to a Champion for one calendar day
Primary Idea The top idea in a pack, shown with a CTA button and visual emphasis
Secondary Idea Supporting ideas in a pack, shown as compact text rows
Pack Date The calendar date (in the Champion’s local timezone) that a pack belongs to
CL Idea A role idea targeting Community Leaders specifically (target: :community_leader)

6. Data Model

6.1 cp_role_ideas Table

Mirrors cp_seeded_questions architecture with role-specific fields.

create_table "cp_role_ideas", force: :cascade do |t|
  # Content
  t.string   "title",       limit: 120              # Optional short title
  t.text     "body",        null: false              # Idea text (supports  interpolation)
  t.string   "cta_label",   limit: 60               # CTA button text (e.g., "Start a discussion")
  t.string   "cta_route"                             # Named route or path fragment (e.g., "cp_discussions_path")

  # Targeting
  t.string   "role",        null: false              # connection_advisor | digital_ambassador | community_builder | giving_advocate
  t.string   "target",      default: "champion"      # champion | community_leader (CL-specific ideas)

  # Management
  t.integer  "status",      default: 0, null: false  # draft:0, active:1, paused:2, archived:3
  t.integer  "priority",    default: 50              # 0-100, higher = more likely to appear
  t.bigint   "created_by_id"
  t.bigint   "updated_by_id"
  t.datetime "created_at",  null: false
  t.datetime "updated_at",  null: false

  t.index ["role", "status"],   name: "index_cp_role_ideas_on_role_and_status"
  t.index ["target", "status"], name: "index_cp_role_ideas_on_target_and_status"
  t.index ["status"],           name: "index_cp_role_ideas_on_status"
  t.index ["created_by_id"],    name: "index_cp_role_ideas_on_created_by_id"
end

Foreign keys:

add_foreign_key "cp_role_ideas", "users", column: "created_by_id"
add_foreign_key "cp_role_ideas", "users", column: "updated_by_id"

6.2 Cp::RoleIdea Model

module Cp
  class RoleIdea < ApplicationRecord
    self.table_name = "cp_role_ideas"

    belongs_to :created_by, class_name: "User", optional: true
    belongs_to :updated_by, class_name: "User", optional: true
    has_many :pack_items, class_name: "Cp::RoleIdeaPackItem",
             foreign_key: :role_idea_id, dependent: :destroy

    enum :status, { draft: 0, active: 1, paused: 2, archived: 3 }, prefix: true

    ROLES = Cp::Champion::CHAMPION_ROLES
    TARGETS = %w[champion community_leader].freeze

    # Template variables for body interpolation
    TEMPLATE_VARIABLES = %w[community_name district_name first_name].freeze

    validates :body, presence: true
    validates :role, presence: true, inclusion: { in: ROLES }
    validates :target, inclusion: { in: TARGETS }
    validates :status, presence: true
    validates :priority, numericality: { only_integer: true,
      greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }, allow_nil: true
    validates :title, length: { maximum: 120 }, allow_blank: true
    validates :cta_label, length: { maximum: 60 }, allow_blank: true

    scope :active, -> { status_active }
    scope :for_role, ->(role) { where(role: role) }
    scope :for_champions, -> { where(target: "champion") }
    scope :for_community_leaders, -> { where(target: "community_leader") }

    # Interpolate template variables into body text
    def render_body(context = {})
      result = body.dup
      TEMPLATE_VARIABLES.each do |var|
        result.gsub!("}", context[var.to_sym].to_s)
      end
      result
    end
  end
end

6.3 cp_role_idea_packs Table

Persists the daily selection per Champion for stability, refresh, and anti-repeat.

create_table "cp_role_idea_packs", force: :cascade do |t|
  t.bigint   "cp_champion_id", null: false
  t.date     "pack_date",      null: false   # Date in Champion's local timezone
  t.string   "role",           null: false   # Role at time of pack generation
  t.integer  "refresh_count",  default: 0    # Times Champion refreshed this day
  t.datetime "created_at",     null: false
  t.datetime "updated_at",     null: false

  t.index ["cp_champion_id", "pack_date"], name: "index_cp_role_idea_packs_on_champion_date", unique: true
  t.index ["pack_date"],                   name: "index_cp_role_idea_packs_on_pack_date"
end

add_foreign_key "cp_role_idea_packs", "cp_champions", column: "cp_champion_id"

6.4 cp_role_idea_pack_items Table

Join table linking a pack to its ordered ideas.

create_table "cp_role_idea_pack_items", force: :cascade do |t|
  t.bigint  "role_idea_pack_id", null: false
  t.bigint  "role_idea_id",      null: false
  t.integer "position",          null: false, default: 0  # 0 = primary, 1-2 = secondary
  t.datetime "created_at",       null: false

  t.index ["role_idea_pack_id"],                 name: "index_pack_items_on_pack_id"
  t.index ["role_idea_pack_id", "role_idea_id"], name: "index_pack_items_on_pack_and_idea", unique: true
end

add_foreign_key "cp_role_idea_pack_items", "cp_role_idea_packs", column: "role_idea_pack_id"
add_foreign_key "cp_role_idea_pack_items", "cp_role_ideas",      column: "role_idea_id"

6.5 Cp::RoleIdeaPack Model

module Cp
  class RoleIdeaPack < ApplicationRecord
    self.table_name = "cp_role_idea_packs"

    belongs_to :champion, class_name: "Cp::Champion", foreign_key: :cp_champion_id
    has_many :items, class_name: "Cp::RoleIdeaPackItem",
             foreign_key: :role_idea_pack_id, dependent: :destroy
    has_many :role_ideas, through: :items

    validates :pack_date, presence: true
    validates :role, presence: true
    validates :cp_champion_id, uniqueness: { scope: :pack_date }

    scope :for_date, ->(date) { where(pack_date: date) }
    scope :recent, ->(days) { where("pack_date >= ?", days.days.ago.to_date) }

    def primary_idea
      items.find_by(position: 0)&.role_idea
    end

    def secondary_ideas
      items.where.not(position: 0).order(:position).map(&:role_idea)
    end
  end
end

6.6 Cp::RoleIdeaPackItem Model

module Cp
  class RoleIdeaPackItem < ApplicationRecord
    self.table_name = "cp_role_idea_pack_items"

    belongs_to :role_idea_pack, class_name: "Cp::RoleIdeaPack",
               foreign_key: :role_idea_pack_id
    belongs_to :role_idea, class_name: "Cp::RoleIdea",
               foreign_key: :role_idea_id

    validates :position, presence: true
    validates :role_idea_id, uniqueness: { scope: :role_idea_pack_id }
  end
end

7. Selection Algorithm

Approach: Persisted Daily Pack

Chosen: Option 1 (Persisted daily selection). This mirrors the existing SeededQuestionExposure pattern and provides:

7.1 Pack Generation Service (Cp::RoleIdeaPackService)

class Cp::RoleIdeaPackService
  PACK_SIZE = 3
  ANTI_REPEAT_WINDOW_DAYS = 30

  def initialize(champion)
    @champion = champion
    @role = champion.primary_role
    @today = today_in_champion_tz
  end

  # Returns an existing pack for today, or generates a new one.
  def today_pack
    existing = Cp::RoleIdeaPack.find_by(cp_champion_id: @champion.id, pack_date: @today)
    return existing if existing&.items&.any?

    generate_pack
  end

  # Generates a new pack (used for first load and refresh).
  def refresh_pack
    existing = Cp::RoleIdeaPack.find_by(cp_champion_id: @champion.id, pack_date: @today)
    if existing
      existing.items.destroy_all
      existing.increment!(:refresh_count)
      populate_pack(existing)
    else
      generate_pack
    end
  end

  private

  def generate_pack
    pack = Cp::RoleIdeaPack.create!(
      cp_champion_id: @champion.id,
      pack_date: @today,
      role: @role
    )
    populate_pack(pack)
  end

  def populate_pack(pack)
    ideas = select_ideas
    ideas.each_with_index do |idea, i|
      pack.items.create!(role_idea: idea, position: i)
    end
    pack.reload
  end

  def select_ideas
    pool = build_candidate_pool
    recently_shown = recently_shown_idea_ids

    # Prefer ideas not shown within the anti-repeat window
    fresh = pool.where.not(id: recently_shown)

    # Graceful degradation: if not enough fresh ideas, allow recent ones
    candidates = if fresh.count >= PACK_SIZE
                   fresh
                 else
                   pool
                 end

    # Weighted random selection: higher priority = higher likelihood
    weighted_select(candidates, PACK_SIZE)
  end

  def build_candidate_pool
    pool = Cp::RoleIdea.status_active.for_role(@role).for_champions

    # If Champion is a Community Leader, also include CL-specific ideas
    if @champion.community_leader?
      pool = Cp::RoleIdea.status_active.where(
        "(role = ? AND target = ?) OR target = ?",
        @role, "champion", "community_leader"
      )
    end

    pool
  end

  def recently_shown_idea_ids
    window = [ANTI_REPEAT_WINDOW_DAYS, available_idea_count_window].min
    Cp::RoleIdeaPackItem
      .joins(:role_idea_pack)
      .where(cp_role_idea_packs: { cp_champion_id: @champion.id })
      .where("cp_role_idea_packs.pack_date >= ?", window.days.ago.to_date)
      .pluck(:role_idea_id)
      .uniq
  end

  # If the library is small, shrink the anti-repeat window to avoid
  # showing nothing. Ensures we always have a full pack.
  def available_idea_count_window
    total = build_candidate_pool.count
    return 0 if total <= PACK_SIZE
    # Allow at most (total - PACK_SIZE) days of exclusion
    [(total - PACK_SIZE) * 2, ANTI_REPEAT_WINDOW_DAYS].min
  end

  def weighted_select(scope, count)
    ideas = scope.to_a
    return ideas.sample(count) if ideas.all? { |i| i.priority == ideas.first.priority }

    selected = []
    remaining = ideas.dup
    count.times do
      break if remaining.empty?
      weights = remaining.map { |i| [i, (i.priority || 50)] }
      total = weights.sum(&:last).to_f
      pick = rand * total
      cumulative = 0
      chosen = weights.detect { |_, w| (cumulative += w) >= pick }&.first || remaining.sample
      selected << chosen
      remaining.delete(chosen)
    end
    selected
  end

  def today_in_champion_tz
    tz = ActiveSupport::TimeZone[@champion.time_zone] || ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
    tz.now.to_date
  end
end

7.2 Day-Boundary & Refresh Rules

Rule Behavior
New day At first dashboard load after midnight in Champion’s time_zone, a new pack is generated
Same day revisit Returns the persisted pack (stable across refreshes)
Refresh button Generates a new pack for the same day; increments refresh_count; respects anti-repeat
Role change Next pack generation uses the new role; old packs retained for analytics

7.3 Anti-Repeat Behavior

Library Size Window Behavior
20+ per role 30 days Full anti-repeat; no idea repeats within last 30 days
10–19 per role Adaptive Window shrinks proportionally; some repeats after a week
< 10 per role Minimal Small window; repeats acceptable; log a warning for admins

8. Dashboard Placement & Card States

8.1 Placement

The card is inserted in the right column of the dashboard, between the existing Upcoming Events card and the Your Communities card.

Right column order after Phase 11:

  1. My District
  2. Upcoming Events
  3. Your Champion Role ← new
  4. Your Communities
  5. Messages
  6. Your Profile

Mobile behavior: On small viewports, the right column renders above the left column (existing order-1 lg:order-2 pattern). The role card will inherit this ordering and appear in the natural flow.

8.2 Visibility

Champion Status Role State Card Shown?
email_verified No role ✅ State A
email_verified Role selected ✅ State B
champion_verified No role ✅ State A
champion_verified Role selected ✅ State B
pending / suspended Any ❌ Not shown

8.3 State A: No Role Selected

Shown when champion.primary_role.blank?.

Card layout:

┌─────────────────────────────────────┐
│ 🎯  Your Champion Role              │
├─────────────────────────────────────┤
│                                     │
│  Knowing your role helps you name   │
│  how you already show up for        │
│  others — and opens the door to     │
│  ideas you can act on right away.   │
│                                     │
│  ┌─────────────────────────────┐    │
│  │     Discover your role      │    │
│  └─────────────────────────────┘    │
│                                     │
│  Learn more about the roles →       │
│                                     │
└─────────────────────────────────────┘

Copy (final):

Element Text
Card header Your Champion Role
Body Knowing your role helps you name how you already show up for others — and opens the door to ideas you can act on right away.
Primary CTA button Discover your role
Secondary link Learn more about the roles →

Routes:

Acceptance criteria:

8.4 State B: Role Selected — Daily Idea Pack

Shown when champion.primary_role.present?.

Card layout:

┌─────────────────────────────────────┐
│ 🎯  Community Builder               │
│     Ideas for today                 │
├─────────────────────────────────────┤
│                                     │
│  [PRIMARY IDEA]                     │
│  Post a welcome for a new member    │
│  in Nashville Champions.            │
│  ┌───────────────────────┐          │
│  │  Start a discussion   │          │
│  └───────────────────────┘          │
│                                     │
│  ─────────────────────────────      │
│                                     │
│  · Share a favorite Belmont         │
│    memory in National Board.        │
│                                     │
│  · Invite someone you admire        │
│    to join Champions.               │
│                                     │
│  ─────────────────────────────      │
│  🔄 Refresh ideas                   │
│  ─────────────────────────────      │
│  ✏️ Retake quiz · Edit role          │
└─────────────────────────────────────┘

Copy (final):

Element Text
Card header {Role Name} (e.g., “Community Builder”)
Sub-header Ideas for today
Primary idea Dynamic from Cp::RoleIdea body + CTA
Secondary ideas Dynamic from Cp::RoleIdea body
Refresh link Refresh ideas
Bottom links Retake quiz · Edit role

Routes:

Acceptance criteria:

8.5 Refresh Endpoint

POST /dashboard/refresh_role_ideas

8.6 CTA Route Resolution

The cta_route field stores a route identifier that the card partial resolves to a URL. Supported values:

cta_route Value Resolved Path Description
discussions cp_discussions_path Start a discussion
discussions_new new_cp_discussion_path Create a new discussion post
directory cp_directory_index_path Browse the directory
events cp_events_path Explore events
messages cp_messages_path Send a message
communities cp_communities_path Explore communities
news cp_news_index_path Read latest news
profile_edit edit_cp_profile_path Update your profile
invite new_cp_invite_path Invite someone
roles cp_roles_path Learn about roles
career_center cp_career_center_path Visit career center
community:{slug} cp_community_path(slug) Link to specific community
nil / blank no button Text-only idea

A helper method resolve_cta_route(idea, champion) handles this mapping and substitutes dynamic segments (e.g., community slug from the Champion’s memberships).


9. Roles Landing Page

9.1 Overview

A new page at /roles accessible to all logged-in Champions (including email_verified). This is the destination for the “Learn more about the roles” link in State A and a reusable educational resource.

Route: GET /rolesCp::RolesController#index

9.2 Page Content

All copy is drawn from ALUMNI_CHAMPIONS__ROLES_FRAMEWORK.md and the Verbal Style Guide. Do not invent new role descriptions or language.

Structure:

┌──────────────────────────────────────────────────┐
│  ← Back to Dashboard                             │
│                                                  │
│  Find Your Champion Role                         │
│                                                  │
│  Roles name how you already show up — they're    │
│  not assignments. Pick the one that feels most    │
│  like you, and we'll give you ideas to get       │
│  started.                                        │
│                                                  │
│  ┌──────────────────────────────────────┐        │
│  │     Discover your role               │        │
│  └──────────────────────────────────────┘        │
│                                                  │
│  ────────────────────────────────────────        │
│                                                  │
│  🏠 Community Builder                             │
│  Create spaces of belonging.                     │
│  "I love bringing people together."              │
│  • Host gatherings                               │
│  • Welcome newcomers and make introductions      │
│  • Help people feel included and at home         │
│                                                  │
│  ────────────────────────────────────────        │
│                                                  │
│  📣 Digital Ambassador                            │
│  Amplify stories and celebrate people online.    │
│  "I'm already sharing and cheering people on."   │
│  • Share alumni wins                             │
│  • Repost Belmont stories                        │
│  • Comment encouragement and connect people      │
│                                                  │
│  ────────────────────────────────────────        │
│                                                  │
│  🤝 Connection Advisor                            │
│  Open doors and share wisdom.                    │
│  "I'm a connector — I like helping people take   │
│   their next steps."                             │
│  • Mentor or coach                               │
│  • Make introductions                            │
│  • Forward job openings                          │
│                                                  │
│  ────────────────────────────────────────        │
│                                                  │
│  💛 Giving Advocate                               │
│  Inspire generosity and connect alumni to        │
│  purpose.                                        │
│  "I care about what Belmont makes possible."     │
│  • Share impact stories before numbers           │
│  • Normalize generosity at every level           │
│  • Thank donors and celebrate outcomes           │
│                                                  │
│  ────────────────────────────────────────        │
│                                                  │
│  You can change your role anytime.               │
│  Start small. Grow over time.                    │
│                                                  │
│  ┌──────────────────────────────────────┐        │
│  │     Discover your role               │        │
│  └──────────────────────────────────────┘        │
│                                                  │
└──────────────────────────────────────────────────┘

9.3 Copy Source

Section Source Document
Page intro ALUMNI_CHAMPIONS__ONBOARDING_PHILOSHOPY.md (“Role selection should feel like discovery, not commitment”)
Role names and descriptions ALUMNI_CHAMPIONS__ROLES_FRAMEWORK.mdquoted verbatim
Signal quotes ALUMNI_CHAMPIONS__ROLES_FRAMEWORK.md “Signals” section
Closing line ALUMNI_CHAMPIONS__OVERVIEW.md (“Start small and grow over time”)

9.4 Acceptance Criteria


10. Admin CRUD

10.1 Location

Admin interface for managing role ideas lives in the Lookup Portal Champion Admin area at:

/champions/role_ideas

Namespace: Champions::RoleIdeasController Access: portal_admin or admin (inherited from Champions::BaseController)

10.2 Routes

# config/routes.rb — inside champions namespace
resources :role_ideas do
  member do
    patch :activate
    patch :deactivate
  end
end

Add “Role Ideas” link to Champion Admin navigation (alongside existing “Seeded Questions” link).

10.3 Index View

Follows the seeded questions index pattern:

Feature Implementation
Tab filters Status tabs: Active (default), Draft, Paused, Archived — with counts
Role filter Dropdown: All Roles, Community Builder, Digital Ambassador, Connection Advisor, Giving Advocate, Community Leader (target filter)
Search ILIKE search on title + body
Sort By created_at (default), priority, title
Pagination Kaminari, 20 per page
Row display Title (or truncated body), role badge, target badge, status badge, priority, created_by, action buttons

10.4 Form Fields

Field Input Type Required Notes
Role Select dropdown Yes 4 Champion roles
Target Select Yes “Champion” (default) or “Community Leader”
Title Text input (120 char) No Optional short title
Body Textarea Yes Supports ,, `` interpolation
CTA Label Text input (60 char) No Button text (e.g., “Start a discussion”)
CTA Route Select dropdown No From supported routes list (§8.6)
Status Select Yes Draft (default), Active, Paused, Archived
Priority Number (0-100) No Default 50; higher = more likely to appear

10.5 Preview

Render a preview of the idea as it would appear in the dashboard card (interpolating sample values for template variables).

10.6 Acceptance Criteria


11. Personalization Rules

Ideas can reference Champion-specific data via template variables and contextual selection logic.

11.1 Template Variables

Variable Source Example
`` One of the Champion’s communities (selected per idea) “Nashville Champions”
`` Champion’s district display name “Nashville”
`` Champion’s display_first_name “Sarah”

11.2 Community Selection for Ideas

When an idea references ``:

  1. If Champion belongs to 1 community → use it
  2. If Champion belongs to multiple → rotate communities across the pack (idea 1 gets community A, idea 2 gets community B, etc.)
  3. If Champion belongs to 0 communities → skip this idea during selection (prefer ideas without community references)

11.3 Activity-Gap Personalization (Future Enhancement)

Phase 11 includes the data model and service hooks but defers activity-gap logic to Phase 11.x:

Phase 11 behavior: All active ideas for the role are weighted equally (by priority field only). Activity-gap weighting is a Phase 11.x enhancement.


12. Community Leader Variant

12.1 How It Works

Community Leaders see a mixed pool of ideas from:

  1. Their Champion role ideas (e.g., Community Builder ideas) with target: "champion"
  2. CL-specific ideas with target: "community_leader" (role-agnostic)

Both pools are combined during pack generation. CL ideas are not filtered by role — they apply to any CL regardless of their Champion role.

12.2 CL-Specific Ideas (Examples)

Idea CTA Route
“Post a welcome thread for new members in .” discussions_new
“Check your moderation queue — keeping conversations healthy helps everyone.” nil (no route; CL knows where to find it)
“Give a shout-out to an active member in .” discussions_new
“Review events coming up in and share one with your network.” events

12.3 Acceptance Criteria


13. Analytics Events

13.1 New Activity Event Types

Add to Cp::ActivityEvent::EVENT_TYPES:

Event Type Trigger Metadata
viewed_role_card Dashboard renders the role card (State A or B) { state: "no_role" \| "role_selected", role: "community_builder" }
clicked_take_quiz Champion clicks “Discover your role” CTA { source: "role_card" \| "roles_page" }
clicked_learn_more_roles Champion clicks “Learn more about the roles” link {}
refreshed_role_ideas Champion clicks “Refresh ideas” { pack_date: "2026-02-12", refresh_count: 1 }
clicked_role_idea_cta Champion clicks a CTA link on an idea { role_idea_id: 42, position: 0, cta_route: "discussions" }
role_selected Champion selects a role (via quiz results or profile edit) { role: "community_builder", source: "quiz" \| "profile_edit" }

13.2 Recording Pattern

Use existing Cp::ActivityRecorder.record pattern:

Cp::ActivityRecorder.record(champion, :clicked_role_idea_cta, {
  role_idea_id: idea.id,
  position: 0,
  cta_route: idea.cta_route
})

13.3 Success Metrics Dashboard (Admin)

Add to existing Champion Metrics area (Lookup portal):

Metric Query
Role selection rate Champion.where.not(primary_role: nil).count / Champion.verified.count
Daily card views (State B) ActivityEvent.where(event_type: "viewed_role_card").where("metadata->>'state' = 'role_selected'").group_by_day(:occurred_at).count
CTA click-through rate clicked_role_idea_cta count / viewed_role_card count
Refresh rate refreshed_role_ideas count / viewed_role_card count
Quiz start rate clicked_take_quiz count / viewed_role_card count (state=no_role)
Downstream actions Track if message_sent, post_created, event_rsvp_clicked events occur within 30 min of a clicked_role_idea_cta

14. User-Facing Copy

All copy follows the LANGUAGE_STYLE_GUIDE.md and draws from ALUMNI_CHAMPIONS__VERBAL_STYLE_GUIDE.md.

14.1 State A Copy

Element Copy
Header Your Champion Role
Body Knowing your role helps you name how you already show up for others — and opens the door to ideas you can act on right away.
Primary CTA Discover your role
Secondary link Learn more about the roles →

Tone notes: Recognitional (“how you already show up”) + action-oriented (“act on right away”). No guilt, no pressure. Per ALUMNI_CHAMPIONS__ONBOARDING_PHILOSHOPY.md: “Role selection should feel like discovery, not commitment.”

14.2 State B Copy

Element Copy
Header {Role Display Name} (e.g., “Community Builder”)
Sub-header Ideas for today
Refresh label Refresh ideas
Bottom links Retake quiz · Edit role
Empty state (if no active ideas) We’re building new ideas for your role. Check back soon!

Tone notes: Direct, celebratory of their role choice. Ideas are invitations, not tasks. Per ALUMNI_CHAMPIONS__OVERVIEW.md: “Champions should always have a clear next action they can take today.”

14.3 Role Display Names

primary_role Value Display Name
connection_advisor Connection Advisor
digital_ambassador Digital Ambassador
community_builder Community Builder
giving_advocate Giving Advocate

14.4 Roles Landing Page Copy

See §9.2 for full page layout. All role descriptions quoted from ALUMNI_CHAMPIONS__ROLES_FRAMEWORK.md.


15. Seed Data Library

15.1 Strategy

Seed data is delivered as a YAML file at db/seeds/role_ideas.yml and loaded via a rake task: bin/rake role_ideas:seed.

Target counts:

15.2 Representative Examples (5 per role)

These examples set the tone and pattern. The full library is in the YAML file.

Community Builder — Belonging Hospitality
Body CTA Label CTA Route
Post a welcome to the newest members of . A simple “glad you’re here” goes a long way. Start a discussion discussions_new
Know someone who’d love being part of this community? Send them an invite. Send an invite invite
Browse the directory and find someone in you haven’t met yet. Say hello. Browse directory directory
Share a favorite gathering spot in that other Champions might enjoy. Start a discussion discussions_new
Drop a note in about something coming up locally — a coffee meetup, a concert, anything. Post in community community:
Digital Ambassador — Storytelling Celebration
Body CTA Label CTA Route
Spot a fellow Bruin doing something great? Share their story in . Share a story discussions_new
Check out the latest news and share something that inspires you with your network. Read the news news
Post an encouraging comment on a recent discussion in . Every voice matters. Visit discussions discussions
Share a “then and now” memory — what Belmont meant to you then and what it means to you now. Start a discussion discussions_new
Celebrate a Champion you admire. Tag them in a post or send them a message. Browse directory directory
Connection Advisor — Guidance Opportunity
Body CTA Label CTA Route
Someone in the directory might need exactly the kind of advice you can give. Browse and reach out. Browse directory directory
Share a career resource or insight in that helped you take your next step. Start a discussion discussions_new
Check out the Career Center — there may be something worth sharing with a younger alum. Visit Career Center career_center
Think of one introduction you could make this week. Two alumni who should know each other. Send a message messages
Post a “here’s what I wish I’d known” tip in . Start a discussion discussions_new
Giving Advocate — Generosity Gratitude
Body CTA Label CTA Route
Share a story about how Belmont shaped your path. Stories inspire generosity more than numbers do. Start a discussion discussions_new
Thank someone in for their contribution — a kind word costs nothing and means everything. Post in community discussions_new
Read the latest news and share an impact update with someone who cares about Belmont’s mission. Read the news news
Post a reflection about a professor, mentor, or moment at Belmont that changed your direction. Start a discussion discussions_new
Encourage a fellow Champion by sharing why you give back — at any level, time, talent, or treasure. Start a discussion discussions_new

Community Leader (CL-specific)

Body CTA Label CTA Route
Post a welcome thread for new members who joined recently. Start a discussion discussions_new
Review your moderation queue — keeping conversations healthy helps everyone feel safe.    
Give a shout-out to an active member in . Recognition sparks more participation. Start a discussion discussions_new
Share an upcoming event in and invite members to RSVP. View events events
Start a “get to know you” thread in — simple questions build real connection. Start a discussion discussions_new

15.3 Seed Rake Task

# lib/tasks/role_ideas.rake
namespace :role_ideas do
  desc "Seed role ideas from YAML file"
  task seed: :environment do
    # ... load and create, idempotent via title+role+body match
  end

  desc "Show role idea statistics"
  task stats: :environment do
    # Per-role and per-target counts, active/inactive
  end
end

16. Testing Requirements

16.1 Model Tests

test/models/cp/role_idea_test.rb

test/models/cp/role_idea_pack_test.rb

16.2 Service Tests

test/services/cp/role_idea_pack_service_test.rb

16.3 Controller / Request Tests

test/controllers/cp/dashboard_controller_test.rb (additions)

test/controllers/cp/roles_controller_test.rb

test/controllers/champions/role_ideas_controller_test.rb

16.4 Timezone Coverage

Explicitly test at least these scenarios:

Scenario Champion TZ Server Time (UTC) Expected Pack Date
Late evening Eastern Eastern 2026-02-13 03:00 2026-02-12
Early morning Pacific Pacific 2026-02-13 07:00 2026-02-12
After midnight Central Central 2026-02-13 07:00 2026-02-13
Default timezone Eastern (default) 2026-02-13 04:00 2026-02-12

16.5 Fixture Requirements

Add to test/fixtures/cp/:


17. Definition of Success

Metric Target Measurement Window
Role selection rate 75%+ of active Champions 60 days post-launch
Daily card engagement 30%+ of logged-in Champions click at least one idea per week Ongoing after launch
Quiz completion rate 50%+ of Champions shown State A start the quiz 60 days post-launch
CTA click-through rate 15%+ of idea views lead to a CTA click Ongoing
Idea library size 100+ active ideas across all roles At launch
Refresh usage < 20% refresh rate (indicates pack quality) Ongoing

18. Rollout Notes

18.1 Feature Flag

No feature flag is required. The card renders conditionally based on Champion status (email_verified or champion_verified). If the cp_role_ideas table is empty (no seed data), State B shows the empty-state message (“We’re building new ideas for your role. Check back soon!”).

18.2 Migration Order

  1. Create cp_role_ideas table
  2. Create cp_role_idea_packs table
  3. Create cp_role_idea_pack_items table

All migrations are additive (new tables only); no risk to existing data.

18.3 Post-Deployment Tasks

# Seed the role ideas library
heroku run bin/rake role_ideas:seed --app alumni-lookup

# Verify seed data
heroku run bin/rake role_ideas:stats --app alumni-lookup

18.4 Cleanup

Add a periodic cleanup task (daily via Heroku Scheduler) to purge packs older than 90 days:

# lib/tasks/role_ideas.rake
task cleanup: :environment do
  deleted = Cp::RoleIdeaPack.where("pack_date < ?", 90.days.ago).destroy_all.count
  puts "Cleaned up #{deleted} old role idea packs"
end

19. Documentation Updates

When Phase 11 is complete, update the following:


20. Spec Deviations

Implementation diverged from spec in these areas (approved during visual QA):

Spec Implementation Reason
Card in right column between Events & Communities (§8.1) Card in left column above Discussions More horizontal space for idea content; better visual weight
State A CTA: “Discover your role” “Take the quiz” More direct and action-oriented
State A secondary link: “Learn more about the roles →” “Or choose directly” → profile edit Provides manual role selection without quiz
State B subheader: “Ideas for today” “Ideas for you today” More personal tone
Roles page title: “Find Your Champion Role” “Champion Roles” Simpler; quiz CTA handles the “find” action
No manual role selection on roles page “Choose this role” buttons + POST /roles/select User requested direct selection without quiz
Idea card layout: primary + 2 secondary (stacked) Numbered list (1, 2, 3) with inline CTA links Cleaner visual hierarchy, less cluttered
Edit role link → edit_cp_profile_path(section: "role") edit_cp_profile_path(section: "champion_role") Correct section slug

See AGENTS.md for development patterns, testing requirements, and commit conventions.