|
| 1 | +# Job Matches Enhancement PRD |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Enhance the job matching system in `lib/algora_web/live/org/job_live.ex` to provide better candidate privacy, accurate match counting, improved matching criteria, and visual enhancement with GitHub contribution heatmaps. |
| 6 | + |
| 7 | +## Background |
| 8 | + |
| 9 | +Currently, the job matching system in `lib/algora_web/live/org/job_live.ex` has several limitations: |
| 10 | +1. Matches show full user information (avatar, name, handle) which may not be appropriate for privacy |
| 11 | +2. Match counts are artificially limited (15-50 matches) rather than showing actual match numbers |
| 12 | +3. Matching criteria doesn't properly validate user email availability |
| 13 | +4. Missing visual contribution heatmaps that provide valuable candidate assessment data |
| 14 | + |
| 15 | +## Goals |
| 16 | + |
| 17 | +1. **Anonymize Matches**: Protect candidate privacy by anonymizing user information in match cards |
| 18 | +2. **Accurate Match Counting**: Display real match counts instead of arbitrary limits |
| 19 | +3. **Enhanced Matching Criteria**: Improve matching logic to include email validation |
| 20 | +4. **Visual Enhancement**: Add GitHub contribution heatmaps for better candidate evaluation |
| 21 | + |
| 22 | +## Requirements |
| 23 | + |
| 24 | +### 1. Anonymized Match Display |
| 25 | + |
| 26 | +**Current State:** |
| 27 | +- Matches display full user avatar, name, and GitHub handle |
| 28 | +- All user information is visible in match cards |
| 29 | + |
| 30 | +**Required Changes:** |
| 31 | +- Replace user avatar with a gray circle (`bg-muted` background) |
| 32 | +- Replace user name and handle with "Anonymous user" text |
| 33 | +- Apply `blur-sm` CSS class to anonymized text for visual indication |
| 34 | +- Maintain country flag display if available |
| 35 | +- Keep contribution data and tech stack information visible |
| 36 | + |
| 37 | +**Technical Implementation:** |
| 38 | +```heex |
| 39 | +<!-- Current --> |
| 40 | +<.avatar class="h-12 w-12 rounded-full"> |
| 41 | + <.avatar_image src={@user.avatar_url} alt={@user.name} /> |
| 42 | +</.avatar> |
| 43 | +<span class="font-semibold">{@user.name}</span> |
| 44 | +
|
| 45 | +<!-- Anonymized --> |
| 46 | +<div class="h-12 w-12 rounded-full bg-muted"></div> |
| 47 | +<span class="font-semibold blur-sm">Anonymous user</span> |
| 48 | +``` |
| 49 | + |
| 50 | +### 2. Accurate Match Counting |
| 51 | + |
| 52 | +**Current State:** |
| 53 | +- `Algora.Cloud.truncate_matches/2` limits matches to 15 for non-subscribed orgs |
| 54 | +- `Algora.Cloud.count_matches/1` returns fixed limits (50-100) |
| 55 | +- Display shows truncated count, not actual available matches |
| 56 | + |
| 57 | +**Required Changes:** |
| 58 | +- Modify `get_job_matches/1` in `Algora.Settings` to count all available matches |
| 59 | +- Update match counting logic to return actual numbers instead of arbitrary limits |
| 60 | +- Display format: "X matches found" where X is the real count |
| 61 | +- Maintain existing subscription-based access controls for viewing matches |
| 62 | + |
| 63 | +**Technical Implementation:** |
| 64 | +```elixir |
| 65 | +# In Algora.Settings.get_job_matches/1 |
| 66 | +def get_job_matches(job) do |
| 67 | + total_matches = count_all_matches(job) # New function |
| 68 | + available_matches = get_subscription_limited_matches(job) # Existing logic |
| 69 | + |
| 70 | + %{ |
| 71 | + total_count: total_matches, |
| 72 | + matches: available_matches |
| 73 | + } |
| 74 | +end |
| 75 | + |
| 76 | +# New helper function |
| 77 | +defp count_all_matches(job) do |
| 78 | + [ |
| 79 | + tech_stack: job.tech_stack, |
| 80 | + email_required: true, # New criteria |
| 81 | + count_only: true |
| 82 | + ] |
| 83 | + |> Algora.Cloud.list_top_matches() |
| 84 | + |> length() |
| 85 | +end |
| 86 | +``` |
| 87 | + |
| 88 | +### 3. Enhanced Matching Criteria |
| 89 | + |
| 90 | +**Current State:** |
| 91 | +- Matching logic in `Algora.Cloud.list_top_matches/1` doesn't validate email availability |
| 92 | +- Users without email addresses may be included in matches |
| 93 | + |
| 94 | +**Required Changes:** |
| 95 | +- Add email validation to matching criteria |
| 96 | +- A user should be considered a match if they have either: |
| 97 | + - `user.email` field populated, OR |
| 98 | + - `user.provider_meta["email"]` field populated |
| 99 | +- Update matching query to include this email check |
| 100 | +- Maintain existing tech stack matching logic |
| 101 | + |
| 102 | +**Technical Implementation:** |
| 103 | +```elixir |
| 104 | +# In Algora.Cloud.list_top_matches/1 |
| 105 | +def list_top_matches(opts) do |
| 106 | + # ... existing query setup ... |
| 107 | + |
| 108 | + query = |
| 109 | + query |
| 110 | + |> where([u], not is_nil(u.email) or fragment("? IS NOT NULL", u.provider_meta["email"])) |
| 111 | + # ... rest of existing filters ... |
| 112 | +end |
| 113 | +``` |
| 114 | + |
| 115 | +### 4. GitHub Contribution Heatmaps |
| 116 | + |
| 117 | +**Current State:** |
| 118 | +- `lib/algora_cloud/live/candidates_live.ex` has heatmap implementation |
| 119 | +- Heatmaps are stored in `user_heatmaps` table |
| 120 | +- Async sync functionality exists via `AlgoraCloud.Profiles.sync_heatmap_by/1` |
| 121 | + |
| 122 | +**Required Changes:** |
| 123 | +- Add heatmap display to match cards in `job_live.ex` |
| 124 | +- Implement similar heatmap component as in `candidates_live.ex` |
| 125 | +- Add async heatmap syncing when socket connects for users without heatmap data |
| 126 | +- Display 17-week contribution grid with color-coded activity levels |
| 127 | + |
| 128 | +**Technical Implementation:** |
| 129 | + |
| 130 | +1. **Update socket assigns in `assign_applicants/1`:** |
| 131 | +```elixir |
| 132 | +defp assign_applicants(socket) do |
| 133 | + # ... existing code ... |
| 134 | + |
| 135 | + # Fetch heatmaps for all developers |
| 136 | + heatmaps_map = |
| 137 | + developers |
| 138 | + |> Enum.map(& &1.id) |
| 139 | + |> AlgoraCloud.Profiles.list_heatmaps() |
| 140 | + |> Map.new(fn heatmap -> {heatmap.user_id, heatmap.data} end) |
| 141 | + |
| 142 | + # Trigger async sync for missing heatmaps |
| 143 | + missing_heatmap_users = |
| 144 | + developers |
| 145 | + |> Enum.reject(&Map.has_key?(heatmaps_map, &1.id)) |
| 146 | + |
| 147 | + if connected?(socket) and length(missing_heatmap_users) > 0 do |
| 148 | + enqueue_heatmap_sync(missing_heatmap_users) |
| 149 | + end |
| 150 | + |
| 151 | + socket |
| 152 | + |> assign(:heatmaps_map, heatmaps_map) |
| 153 | + # ... rest of existing assigns ... |
| 154 | +end |
| 155 | + |
| 156 | +defp enqueue_heatmap_sync(users) do |
| 157 | + Task.start(fn -> |
| 158 | + for user <- users do |
| 159 | + AlgoraCloud.Profiles.sync_heatmap_by(id: user.id) |
| 160 | + end |
| 161 | + end) |
| 162 | +end |
| 163 | +``` |
| 164 | + |
| 165 | +2. **Add heatmap component to match cards:** |
| 166 | +```heex |
| 167 | +<!-- In match_card function --> |
| 168 | +<div :if={@heatmaps_map[@user.id]} class="mt-4"> |
| 169 | + <.heatmap_display user_id={@user.id} heatmap_data={@heatmaps_map[@user.id]} /> |
| 170 | +</div> |
| 171 | +``` |
| 172 | + |
| 173 | +3. **Implement heatmap component (based on candidates_live.ex):** |
| 174 | +```heex |
| 175 | +defp heatmap_display(assigns) do |
| 176 | + ~H""" |
| 177 | + <div class="mt-4"> |
| 178 | + <div class="flex items-center justify-between mb-2"> |
| 179 | + <div class="text-xs text-muted-foreground uppercase font-semibold"> |
| 180 | + {get_in(@heatmap_data, ["totalContributions"])} contributions in the last year |
| 181 | + </div> |
| 182 | + </div> |
| 183 | + <div class="grid grid-cols-[repeat(17,1fr)] gap-1"> |
| 184 | + <%= for week <- get_in(@heatmap_data, ["weeks"]) |> Enum.take(-17) do %> |
| 185 | + <div class="grid grid-rows-7 gap-1"> |
| 186 | + <%= for day <- week["contributionDays"] do %> |
| 187 | + <div |
| 188 | + class={"h-3 w-3 rounded-sm #{get_contribution_color(day["contributionCount"])}"} |
| 189 | + title={"#{day["contributionCount"]} contributions on #{format_date(day["date"])}"} |
| 190 | + > |
| 191 | + </div> |
| 192 | + <% end %> |
| 193 | + </div> |
| 194 | + <% end %> |
| 195 | + </div> |
| 196 | + </div> |
| 197 | + """ |
| 198 | +end |
| 199 | +
|
| 200 | +defp get_contribution_color(count) do |
| 201 | + cond do |
| 202 | + count == 0 -> "bg-muted/50" |
| 203 | + count in 1..5 -> "bg-success-400/40" |
| 204 | + count in 6..10 -> "bg-success-400/50" |
| 205 | + count in 11..15 -> "bg-success-400/70" |
| 206 | + count in 16..20 -> "bg-success-400/90" |
| 207 | + true -> "bg-success-400" |
| 208 | + end |
| 209 | +end |
| 210 | +``` |
| 211 | + |
| 212 | +## Implementation Plan |
| 213 | + |
| 214 | +### Phase 1: Anonymization and Match Counting |
| 215 | +1. Update `match_card/1` function to display anonymized user information |
| 216 | +2. Modify `Algora.Settings.get_job_matches/1` to return actual match counts |
| 217 | +3. Update UI to display real match numbers |
| 218 | +4. Test anonymization with different user types and subscription levels |
| 219 | + |
| 220 | +### Phase 2: Enhanced Matching Criteria |
| 221 | +1. Update `Algora.Cloud.list_top_matches/1` to include email validation |
| 222 | +2. Add database query filters for email requirements |
| 223 | +3. Update matching logic to check both `user.email` and `user.provider_meta["email"]` |
| 224 | +4. Test matching accuracy with email filtering |
| 225 | + |
| 226 | +### Phase 3: Heatmap Integration |
| 227 | +1. Add heatmap data fetching to `assign_applicants/1` |
| 228 | +2. Implement async heatmap syncing for missing data |
| 229 | +3. Add heatmap component to match cards |
| 230 | +4. Test heatmap display and async syncing functionality |
| 231 | + |
| 232 | +### Phase 4: Testing and Optimization |
| 233 | +1. Performance testing with large match sets |
| 234 | +2. UI/UX testing for anonymized cards |
| 235 | +3. Async operation monitoring for heatmap syncing |
| 236 | +4. Cross-browser compatibility testing |
| 237 | + |
| 238 | +## Success Metrics |
| 239 | + |
| 240 | +1. **Privacy Protection**: All match cards show anonymized information for non-subscribed viewers |
| 241 | +2. **Accurate Counts**: Match counts reflect actual available candidates, not artificial limits |
| 242 | +3. **Better Matching**: Only users with email addresses are included in matches |
| 243 | +4. **Enhanced UX**: Heatmaps provide visual contribution data for better candidate assessment |
| 244 | +5. **Performance**: Async heatmap syncing doesn't impact page load times |
| 245 | + |
| 246 | +## Technical Considerations |
| 247 | + |
| 248 | +### Database Impact |
| 249 | +- Heatmap syncing may increase database load |
| 250 | +- Consider implementing rate limiting for async sync operations |
| 251 | +- Monitor performance of email filtering queries |
| 252 | + |
| 253 | +### Caching Strategy |
| 254 | +- Cache heatmap data to reduce GitHub API calls |
| 255 | +- Implement TTL for heatmap data (suggested: 24 hours) |
| 256 | +- Cache match counts to improve performance |
| 257 | + |
| 258 | +### Error Handling |
| 259 | +- Handle GitHub API rate limits gracefully |
| 260 | +- Fallback display when heatmap data is unavailable |
| 261 | +- Graceful degradation if anonymization fails |
| 262 | + |
| 263 | +### Security Considerations |
| 264 | +- Ensure anonymization cannot be bypassed client-side |
| 265 | +- Validate subscription status server-side for match access |
| 266 | +- Protect GitHub API tokens used for heatmap syncing |
| 267 | + |
| 268 | +## Files to Modify |
| 269 | + |
| 270 | +1. **`lib/algora_web/live/org/job_live.ex`** - Main implementation file |
| 271 | +2. **`lib/algora/settings/settings.ex`** - Match counting logic |
| 272 | +3. **`lib/algora_cloud/algora_cloud.ex`** - Matching criteria enhancement |
| 273 | +4. **`lib/algora_cloud/profiles.ex`** - Heatmap syncing (if needed) |
| 274 | + |
| 275 | +## Dependencies |
| 276 | + |
| 277 | +- Existing `AlgoraCloud.Profiles` module for heatmap functionality |
| 278 | +- `user_heatmaps` database table |
| 279 | +- GitHub GraphQL API access for contribution data |
| 280 | +- Existing subscription and authentication systems |
| 281 | + |
| 282 | +## Rollout Strategy |
| 283 | + |
| 284 | +1. **Development**: Implement features in feature branch |
| 285 | +2. **Staging**: Test with sample job postings and user data |
| 286 | +3. **Gradual Rollout**: Deploy to subset of organizations initially |
| 287 | +4. **Full Deployment**: Roll out to all organizations after validation |
| 288 | +5. **Monitoring**: Track performance metrics and user feedback |
| 289 | + |
| 290 | +## Future Enhancements |
| 291 | + |
| 292 | +1. **Advanced Anonymization**: Different levels of anonymization based on subscription tiers |
| 293 | +2. **Enhanced Heatmaps**: Additional metrics like PR review activity, issue contributions |
| 294 | +3. **Real-time Updates**: Live updating of match counts and heatmap data |
| 295 | +4. **Export Functionality**: Allow exporting of match data for subscribed organizations |
0 commit comments