alumni_lookup

Sub-Phase 1.8: Staff Metrics Dashboard & Notifications

Champion Portal Development Sub-Phase 1.8

Status:COMPLETE (December 23, 2025)
Actual Completion: Admin Dashboard & Engagement Scoring

Estimated Effort: 1–2 weeks
Focus: Staff-facing metrics dashboard, activity analytics, exports, notifications

Prerequisites: Phase 1.6 complete (activity tracking infrastructure exists)

Related Documents:


Table of Contents

  1. Completion Summary
  2. Overview
  3. Why This Sub-Phase Exists
  4. Internal Sub-Phases
  5. Scope
  6. Definition of Success
  7. Tests to Create
  8. Documentation Updates

Completion Summary

Phase 1.8 completed December 23, 2025. This phase evolved from the original staff metrics dashboard plan into a focused Admin Dashboard & Engagement Scoring System.

What Was Implemented

Admin Dashboard:

Engagement Scoring System:

Staff Metrics Dashboard, Exports, and Notifications:

Testing:

Documentation:

What Was Deferred

The items originally listed here as “deferred” (weekly digest, signup notifications, CSV export, charts/trends, district breakdown, and recognition widgets) are implemented in the current codebase. Future enhancements (more event types, deeper segmentation, scheduled reporting flexibility) can build on this in Phase 6.


1. Overview

Phase 1B (Metrics Foundation) was marked as “absorbed” into Phase 1.6, but Phase 1.8 completes the staff-facing layer: a metrics dashboard, exports, and notifications built on cp_activity_events.

This sub-phase includes:

After Phase 1.8, the Engagement Team can:


2. Why This Sub-Phase Exists

From JOBS-TO-BE-DONE.md:

Job E2: See the Big Picture

“When I’m reporting to leadership or planning strategy, I want to see Champion activity metrics at a glance, so I can demonstrate value and identify opportunities.”

Importance: 🔥 Critical
Current Satisfaction: ❌ Very Low
Opportunity Score: 🎯 High

Current Gap:

The cp_activity_events table is being populated with:

Before Phase 1.8, staff had no visibility into this data (the /champions/stats page was a placeholder).

Staff visibility is now provided via the /champions/stats dashboard and admin digest/notification emails.


3. Internal Sub-Phases

Sub-Phase Name Est. Time
1.8.1 Signup Notification Emails 0.5 days
1.8.2 Staff Metrics Dashboard 2–3 days
1.8.3 District/City Breakdown 1–2 days
1.8.4 Exports & Weekly Digest 1–2 days
1.8.5 Recognition Features 1 day
1.8.6 Invite Flow 1 day
1.8.7 Empty District Messaging 0.5 days
1.8.8 Archive 1A Update 0.25 days

Sub-Phase 1.8.1: Signup Notification Emails

Goal: Notify portal admins and admins immediately when a new Champion signs up.

Deliverables:

Email Content:

Subject: New Champion Signup: [First Last] — [Needs Verification | Auto-Verified]

Hi [Admin Name],

A new Champion has signed up for the Alumni Champions Portal.

📋 Champion Details:
• Name: [First Last]
• Email: [email@example.com]
• Location: [City, State] ([District Name])
• Signed Up: [Date/Time]
• Sign Up Method: [Email | Google SSO]

🔗 Alumni Match Status:
[IF AUTO-VERIFIED via email match:]
✅ Automatically Verified
• Matched to: [Alumni Name] (BUID: [BUID])
• Match Type: Email address match
• No action required

[IF PENDING VERIFICATION via name match:]
⏳ Needs Verification
• Pending Match: [Alumni Name] (BUID: [BUID])
• Match Type: Name-based (requires staff approval)
• Action: Review and approve in Verification Queue

[IF NO MATCH:]
❓ No Alumni Match Found
• Action: Search manually in Verification Queue

[View in Verification Queue →]

---
You received this email because you are a portal admin for Alumni Lookup.

Implementation:

# app/mailers/cp/admin_notification_mailer.rb
module Cp
  class AdminNotificationMailer < ApplicationMailer
    def new_champion_signup(champion)
      @champion = champion
      @admins = User.where(role: [:admin, :portal_admin])
      
      # Determine verification status
      @status = if champion.champion_verified?
        :auto_verified
      elsif champion.pending_buid.present?
        :pending_verification
      else
        :no_match
      end
      
      mail(
        to: @admins.pluck(:email),
        subject: "New Champion Signup: #{champion.display_name}#{status_label(@status)}"
      )
    end
    
    private
    
    def status_label(status)
      case status
      when :auto_verified then "Auto-Verified"
      when :pending_verification then "Needs Verification"
      else "No Match Found"
      end
    end
  end
end

Trigger Point:

Hook into the email confirmation flow (after Champion confirms email, not on initial form submission):

# In Cp::ConfirmationsController or via callback
after_action :notify_admins_of_new_signup, only: [:show], if: :champion_just_confirmed?

def notify_admins_of_new_signup
  Cp::NotifyAdminsJob.perform_later(@champion.id)
end

Tests:


Sub-Phase 1.8.2: Staff Metrics Dashboard

Goal: Replace the “Coming Soon” placeholder with a comprehensive metrics dashboard.

Deliverables:

Dashboard Layout:

┌─────────────────────────────────────────────────────────────────────────┐
│ Champion Portal Statistics                     [7d ▼] [Export CSV]     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐        │
│ │   Total     │ │   Active    │ │  Directory  │ │  Messages   │        │
│ │  Champions  │ │ This Period │ │  Searches   │ │    Sent     │        │
│ │     127     │ │     89      │ │    156      │ │     23      │        │
│ │  +5 vs prev │ │   70% rate  │ │ +12% ↑      │ │  +8% ↑      │        │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘        │
│                                                                         │
├─────────────────────────────────────────────────────────────────────────┤
│ Activity Trend                                                          │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │     ╭──╮                                                            │ │
│ │    ╭╯  ╰╮   ╭──╮                                                    │ │
│ │ ──╯      ╰──╯  ╰──                                                  │ │
│ │ Mon  Tue  Wed  Thu  Fri  Sat  Sun                                   │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ [Logins] [Searches] [Messages] [Profile Views]                          │
├─────────────────────────────────────────────────────────────────────────┤
│ Recent Activity                          │ Quick Stats                  │
│ • Sarah J. updated profile — 2h ago      │ New This Month: 12          │
│ • Mike T. searched directory — 3h ago    │ Verification Rate: 94%      │
│ • Lisa R. sent message to Tom — 5h ago   │ Profile Completion: 78%     │
│ • John D. logged in — 6h ago             │ Avg Sessions/Week: 2.3      │
└─────────────────────────────────────────────────────────────────────────┘

Metrics Cards (from cp_activity_events):

Metric Query
Total Champions Cp::Champion.verified.count
Active This Period Cp::ActivityEvent.where(occurred_at: period).select(:cp_champion_id).distinct.count
Directory Searches Cp::ActivityEvent.where(event_type: 'directory_search', occurred_at: period).count
Messages Sent Cp::ActivityEvent.where(event_type: 'message_sent', occurred_at: period).count
Profile Views Cp::ActivityEvent.where(event_type: 'profile_view', occurred_at: period).count
Logins Cp::ActivityEvent.where(event_type: 'login', occurred_at: period).count

Date Range Selector:

# params[:period] = '7d' | '30d' | '90d' | 'all'
def period_range
  case params[:period]
  when '7d' then 7.days.ago..Time.current
  when '30d' then 30.days.ago..Time.current
  when '90d' then 90.days.ago..Time.current
  else nil # all time
  end
end

Activity Trend Chart:

Use Chart.js (already in project via importmap). Line chart showing daily activity count by type.

# Controller prepares chart data
@chart_data = {
  labels: (period_start.to_date..Date.current).map(&:to_s),
  datasets: [
    {
      label: 'Logins',
      data: daily_counts_for('login'),
      borderColor: '#3B82F6'
    },
    {
      label: 'Searches',
      data: daily_counts_for('directory_search'),
      borderColor: '#10B981'
    },
    {
      label: 'Messages',
      data: daily_counts_for('message_sent'),
      borderColor: '#F59E0B'
    }
  ]
}

Recent Activity Feed:

Query cp_activity_events and display human-readable actions:

@recent_activity = Cp::ActivityEvent
  .includes(:cp_champion)
  .order(occurred_at: :desc)
  .limit(10)

# View helper
def activity_description(event)
  champion = event.cp_champion.display_name
  case event.event_type
  when 'login' then "#{champion} logged in"
  when 'profile_edit' then "#{champion} updated their profile"
  when 'profile_view' 
    target = Cp::Champion.find_by(id: event.metadata['target_champion_id'])&.display_name || 'another Champion'
    "#{champion} viewed #{target}'s profile"
  when 'directory_search' then "#{champion} searched the directory"
  when 'message_sent' then "#{champion} sent a message"
  when 'message_thread_started' then "#{champion} started a conversation"
  end
end

Service Class:

# app/services/cp/metrics_service.rb
module Cp
  class MetricsService
    def initialize(period: nil)
      @period = period
    end
    
    def overview
      {
        total_champions: Cp::Champion.verified.count,
        active_champions: active_champion_count,
        directory_searches: event_count('directory_search'),
        messages_sent: event_count('message_sent'),
        profile_views: event_count('profile_view'),
        logins: event_count('login')
      }
    end
    
    def daily_activity(event_type)
      scope = Cp::ActivityEvent.where(event_type: event_type)
      scope = scope.where(occurred_at: @period) if @period
      scope.group("DATE(occurred_at)")
           .order("DATE(occurred_at)")
           .count
    end
    
    def recent_activity(limit: 10)
      Cp::ActivityEvent
        .includes(:cp_champion)
        .order(occurred_at: :desc)
        .limit(limit)
    end
    
    private
    
    def active_champion_count
      scope = Cp::ActivityEvent.select(:cp_champion_id).distinct
      scope = scope.where(occurred_at: @period) if @period
      scope.count
    end
    
    def event_count(event_type)
      scope = Cp::ActivityEvent.where(event_type: event_type)
      scope = scope.where(occurred_at: @period) if @period
      scope.count
    end
  end
end

Acceptance Test:

  1. Navigate to /champions/stats → see real metrics (no “Coming Soon”)
  2. Change date range to 7d → metrics update
  3. See activity trend chart with multiple lines
  4. Recent activity shows actual Champion actions
  5. Compare vs previous period percentages

Sub-Phase 1.8.3: District/City Breakdown

Goal: Show Champion distribution and activity by district/city.

Deliverables:

District Breakdown Table:

District Champions Active (30d) Searches Messages Activity Level
Nashville 34 28 (82%) 89 12 🟢 High
Atlanta 18 12 (67%) 45 8 🟡 Medium
Dallas 12 5 (42%) 18 2 🟠 Low
Portland 1 1 (100%) 3 0 🔵 New

Query:

def district_breakdown
  District.joins(:cp_champions)
          .where(cp_champions: { verification_status: :champion_verified })
          .group('districts.id', 'districts.name', 'districts.state')
          .select(
            'districts.id',
            'districts.name',
            'districts.state',
            'COUNT(DISTINCT cp_champions.id) as champion_count'
          )
          .order('champion_count DESC')
          .limit(25)
          .map do |district|
            {
              id: district.id,
              name: district.name,
              state: district.state,
              champion_count: district.champion_count,
              active_count: active_in_district(district.id),
              search_count: searches_in_district(district.id),
              message_count: messages_in_district(district.id)
            }
          end
end

def active_in_district(district_id)
  Cp::ActivityEvent
    .joins(:cp_champion)
    .where(cp_champions: { district_id: district_id })
    .where(occurred_at: 30.days.ago..Time.current)
    .select(:cp_champion_id)
    .distinct
    .count
end

Activity Level Indicators:

Level Criteria
🟢 High > 70% active in 30d
🟡 Medium 40-70% active
🟠 Low < 40% active
🔵 New District added in last 30d

Sub-Phase 1.8.4: Exports & Weekly Digest

Goal: Enable metric exports and automated email summaries.

Deliverables:

CSV Export:

Export button on /champions/stats page. Generates CSV with:

Metric,Value,Period
Total Champions,127,All Time
Verified This Month,12,December 2025
Active Champions (30d),89,Last 30 Days
Directory Searches (30d),156,Last 30 Days
Messages Sent (30d),23,Last 30 Days
...

District,State,Champions,Active (30d),Searches,Messages
Nashville,TN,34,28,89,12
Atlanta,GA,18,12,45,8
...

Controller:

def export
  @service = Cp::MetricsService.new(period: period_range)
  
  respond_to do |format|
    format.csv do
      send_data Cp::MetricsCsvExporter.new(@service).generate,
                filename: "champion-metrics-#{Date.current}.csv",
                type: 'text/csv'
    end
  end
end

Weekly Digest Email:

Subject: Champion Portal Weekly Summary — Dec 16-22, 2025

Hi [Name],

Here's this week's Champion Portal activity:

📊 Key Metrics
• 5 new Champions verified (127 total)
• 89 Champions active this week (70% engagement rate)
• 156 directory searches (+12% vs last week)
• 23 messages sent between Champions

🏙️ Most Active Districts
1. Nashville, TN (34 active Champions)
2. Atlanta, GA (18 active)
3. Dallas, TX (12 active)

🌟 Highlights
• First Champion verified in Portland, OR
• Highest daily activity: Tuesday (47 logins)
• Top Connector: Sarah J. (12 messages sent)

📈 Trends
• Signups: ↑ 15% vs previous week
• Engagement: ↑ 8% vs previous week

View full dashboard: https://alumnilookup.com/champions/stats

---
You received this email because you are a portal admin for Alumni Lookup.
To unsubscribe, update your notification preferences in Settings.

Scheduled Job:

# app/jobs/cp/weekly_digest_job.rb
module Cp
  class WeeklyDigestJob < ApplicationJob
    queue_as :default
    
    def perform
      recipients = User.where(role: [:admin, :portal_admin])
      
      recipients.each do |user|
        Cp::AdminNotificationMailer.weekly_digest(user).deliver_later
      end
    end
  end
end

# config/initializers/good_job.rb or sidekiq-scheduler
# Run every Monday at 8:00 AM Central
GoodJob::Cron.set('weekly_champion_digest', cron: '0 8 * * 1', class: 'Cp::WeeklyDigestJob')

Sub-Phase 1.8.5: Recognition Features

Goal: Surface recognition opportunities to encourage Champion engagement.

Per JOBS-TO-BE-DONE.md Job C10 (Feel Recognized):

“When I’ve been active in the community, I want recognition of my contributions, so I feel valued and motivated to continue.”

Deliverables:

Top Connectors (Last 30 Days):

def top_connectors(limit: 5)
  Cp::ActivityEvent
    .where(event_type: 'message_sent')
    .where(occurred_at: 30.days.ago..Time.current)
    .group(:cp_champion_id)
    .order('COUNT(*) DESC')
    .limit(limit)
    .count
    .map do |champion_id, count|
      champion = Cp::Champion.find(champion_id)
      { champion: champion, message_count: count }
    end
end

Most Viewed Profiles:

def most_viewed_profiles(limit: 5)
  Cp::ActivityEvent
    .where(event_type: 'profile_view')
    .where(occurred_at: 30.days.ago..Time.current)
    .where("metadata->>'target_champion_id' IS NOT NULL")
    .group("metadata->>'target_champion_id'")
    .order('COUNT(*) DESC')
    .limit(limit)
    .count
    .map do |champion_id, count|
      champion = Cp::Champion.find_by(id: champion_id)
      { champion: champion, view_count: count } if champion
    end.compact
end

Dashboard Widget:

┌─────────────────────────────────────────┐
│ 🏆 Recognition                          │
├─────────────────────────────────────────┤
│ Top Connectors (30d)                    │
│ 1. Sarah J. — 12 messages               │
│ 2. Mike T. — 9 messages                 │
│ 3. Lisa R. — 7 messages                 │
├─────────────────────────────────────────┤
│ Most Viewed Profiles                    │
│ 1. John D. — 24 views                   │
│ 2. Emily K. — 18 views                  │
│ 3. Tom S. — 15 views                    │
├─────────────────────────────────────────┤
│ District Leaders                        │
│ Nashville: Sarah J. (28 activities)     │
│ Atlanta: Mike T. (19 activities)        │
│ Dallas: Lisa R. (12 activities)         │
└─────────────────────────────────────────┘

Future Consideration (Phase 6):


Sub-Phase 1.8.6: Invite Flow

Goal: Enable Champions to invite other Belmont alumni to join the Champion Portal.

Per JOBS-TO-BE-DONE.md — supports C9 (Feel Like I Belong) and overall community growth.

Deliverables:

Invite Link Format:

https://alumnichampions.com/signup?ref=[champion_id]

Invite Page UI:

┌─────────────────────────────────────────────────────────────────────────┐
│ 🎓 Invite a Fellow Bruin                                                │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│ Know a Belmont alum who'd make a great Champion?                       │
│ Invite them to join the community!                                      │
│                                                                         │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Option 1: Share Your Link                                           │ │
│ │                                                                     │ │
│ │ https://alumnichampions.com/signup?ref=abc123    [Copy Link]       │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│                                                                         │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Option 2: Send an Email Invite                                      │ │
│ │                                                                     │ │
│ │ Email: [_________________________]                                  │ │
│ │                                                                     │ │
│ │ Personal note (optional):                                           │ │
│ │ [_____________________________________________]                     │ │
│ │                                                                     │ │
│ │ [Send Invite]                                                       │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Invite Email Template:

Subject: [Champion Name] invites you to join Belmont Alumni Champions

Hi there,

[Champion Name], a fellow Belmont alum, thinks you'd be a great addition 
to the Alumni Champions community!

[IF PERSONAL NOTE:]
They said: "[Personal note]"

Alumni Champions is a community of engaged Belmont graduates who:
• Connect with fellow Bruins in their city
• Host and attend local alumni gatherings  
• Support prospective and current students
• Stay connected to the Belmont family

Join [Champion Name] and [X] other Champions:
[Join Now →]

Once a Bruin, always a Bruin. 🐻

---
This invitation was sent by [Champion Name] ([City]).

Database:

# Track invites sent (optional table, or use activity_events)
# For MVP, just use activity_events with metadata
Cp::ActivityRecorder.record(champion, :invite_sent, {
  method: 'email', # or 'link_copy'
  recipient_email: email, # if email invite
  invite_code: code
})

Acceptance Test:

  1. Click “Invite a Fellow Bruin” on dashboard → invite page
  2. Copy link → clipboard contains trackable URL
  3. Enter email + send → invite email sent
  4. Activity event recorded with metadata

Sub-Phase 1.8.7: Empty District Messaging

Goal: Improve dashboard UX when a Champion is the only one (or one of few) in their district.

Current State:

Better Experience:

Dashboard Section When Empty:

┌─────────────────────────────────────────────────────────────────────────┐
│ 🌟 Alumni in [District Name]                                            │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│ You're the first Champion in [District Name]! 🎉                        │
│                                                                         │
│ Help grow the community by inviting Belmont alumni you know            │
│ in the area to join.                                                    │
│                                                                         │
│ [Invite a Fellow Bruin →]                                               │
│                                                                         │
│ ─────────────────────────────────────────────────────────────────────   │
│                                                                         │
│ Nearby Champions:                                                       │
│ • [Nearby District 1] — 3 Champions                                     │
│ • [Nearby District 2] — 1 Champion                                      │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Logic:

Deliverables:


Sub-Phase 1.8.8: Archive 1A Update

Goal: Update the Phase 1A (Quick Connect) archive document to reflect that it was superseded by Phase 1.7 (In-App Messaging).

Current State:

Update Required:

Deliverables:


4. Scope

In Scope

Out of Scope (Phase 6 / Future)


5. Definition of Success

Functional Acceptance Criteria

Staff Experience


6. Tests to Create

Mailer Tests (test/mailers/cp/admin_notification_mailer_test.rb)

Service Tests (test/services/cp/metrics_service_test.rb)

Controller Tests (test/controllers/champions/stats_controller_test.rb)

Job Tests (test/jobs/cp/weekly_digest_job_test.rb)


7. Documentation Updates

After completing Phase 1.8:


Questions Resolved

Question Decision Notes
Dashboard location /champions/stats in Lookup Portal Staff already use Lookup Portal for Champion management
Notification recipients All portal_admin + admin users Configurable per-user in future
Weekly digest schedule Monday 8:00 AM Central Configurable in future
Chart library Chart.js Already in project via importmap
Real-time vs cached Cached with 5-min refresh Real-time would require ActionCable; defer to Phase 6

Files to Create

New Files

Files to Modify