Canonical sources: Portal philosophy, posture, and language live in
/docs/planning/champion-portal/source/README.md. Use these sources (includingCHRIST_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:
- ../README.md — Phase Index
- ../../source/ALUMNI_CHAMPIONS__ROLES_FRAMEWORK.md — Role definitions and portal support ideas
- ../../source/ALUMNI_CHAMPIONS__OVERVIEW.md — Champion thesis, posture, program overview
- ../../source/ALUMNI_CHAMPIONS__ONBOARDING_PHILOSHOPY.md — Onboarding sequencing, “small pack” concept
- ../../source/ALUMNI_CHAMPIONS__VERBAL_STYLE_GUIDE.md — Tone rules, role shorthand
- ../../development/DESIGN-GUIDELINES.md — Dashboard patterns, card design, mobile-first rules
- ../../development/LANGUAGE_STYLE_GUIDE.md — Voice, tone, and word choices for all user-facing copy
- ../../JOBS-TO-BE-DONE.md — Job C10 (Recognition), Job C3 (Clear next step)
- ../phase-12/README.md — Phase 12: Source-Alignment Review (F-01 finding directly informs this phase)
| 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 |
Phase 11 adds a Champion Role card to the Champion Portal dashboard. The card serves two purposes:
ALUMNI_CHAMPIONS__ONBOARDING_PHILOSHOPY.md.“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”).
| 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 |
| 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 |
| 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 |
| 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 |
| 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) |
cp_role_ideas TableMirrors 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"
Cp::RoleIdea Modelmodule 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
cp_role_idea_packs TablePersists 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"
cp_role_idea_pack_items TableJoin 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"
Cp::RoleIdeaPack Modelmodule 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
Cp::RoleIdeaPackItem Modelmodule 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
Chosen: Option 1 (Persisted daily selection). This mirrors the existing SeededQuestionExposure pattern and provides:
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
| 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 |
| 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 |
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:
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.
| 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 |
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:
cp_profile_wizard_quiz_path(question: 1) (existing quiz start)cp_roles_path (new roles landing page, see §9)Acceptance criteria:
email_verified Champions without a rolechampion_verified Champions without a role/profile/wizard/quiz/1/roles landing pageShown 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:
cta_route field (see §6.1)POST /dashboard/refresh_role_ideas (Turbo Stream)cp_profile_wizard_quiz_path(question: 1)edit_cp_profile_path(section: "role")Acceptance criteria:
,, ``) are interpolated correctlycta_route show a CTA button; ideas without show text onlyPOST /dashboard/refresh_role_ideas
Cp::RoleIdeaPackService.new(champion).refresh_packrefreshed_role_ideas activity eventThe 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).
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 /roles → Cp::RolesController#index
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 │ │
│ └──────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────┘
| 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.md — quoted verbatim |
| Signal quotes | ALUMNI_CHAMPIONS__ROLES_FRAMEWORK.md “Signals” section |
| Closing line | ALUMNI_CHAMPIONS__OVERVIEW.md (“Start small and grow over time”) |
/roles for all logged-in ChampionsAdmin 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)
# 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).
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 |
| 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 |
Render a preview of the idea as it would appear in the dashboard card (interpolating sample values for template variables).
Ideas can reference Champion-specific data via template variables and contextual selection logic.
| 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” |
When an idea references ``:
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.
Community Leaders see a mixed pool of ideas from:
target: "champion"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.
| 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 |
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" } |
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
})
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 |
All copy follows the LANGUAGE_STYLE_GUIDE.md and draws from ALUMNI_CHAMPIONS__VERBAL_STYLE_GUIDE.md.
| 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.”
| 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.”
primary_role Value |
Display Name |
|---|---|
connection_advisor |
Connection Advisor |
digital_ambassador |
Digital Ambassador |
community_builder |
Community Builder |
giving_advocate |
Giving Advocate |
See §9.2 for full page layout. All role descriptions quoted from ALUMNI_CHAMPIONS__ROLES_FRAMEWORK.md.
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:
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 |
# 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
test/models/cp/role_idea_test.rb
CHAMPION_ROLESTARGETSactive, for_role, for_champions, for_community_leadersrender_body interpolates template variablesrender_body with missing context returns text with empty substitutionstest/models/cp/role_idea_pack_test.rb
primary_idea returns position-0 itemsecondary_ideas returns position 1+ items orderedtest/services/cp/role_idea_pack_service_test.rb
today_pack returns existing pack if one exists for todaytoday_pack generates a new pack if none existsrefresh_pack creates a new pack for the same dayrefresh_pack increments refresh_counttoday_in_champion_tz returns correct date for Eastern, Central, Pacifictest/controllers/cp/dashboard_controller_test.rb (additions)
test/controllers/cp/roles_controller_test.rb
test/controllers/champions/role_ideas_controller_test.rb
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 |
Add to test/fixtures/cp/:
role_ideas.yml — At least 5 per role + 3 CL ideas (20+ total)role_idea_packs.yml — Packs for fixture championsrole_idea_pack_items.yml — Items linking packs to ideas| 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 |
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!”).
cp_role_ideas tablecp_role_idea_packs tablecp_role_idea_pack_items tableAll migrations are additive (new tables only); no risk to existing data.
# 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
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
When Phase 11 is complete, update the following:
phases/README.md) — Add Phase 11 rowapp/controllers/champions/roadmap_controller.rb) — Add Phase 11 entryCp::RoleIdea, Cp::RoleIdeaPack, Cp::RoleIdeaPackItem associationsai/AI_06_CURRENT_STATE_VS_FUTURE_STATE.md) — Mark Phase 11 featuresconfig/faq.yml) — Add “What is my Champion Role?” and “How do the daily ideas work?” entriesImplementation 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.