# Session Handoff - January 3, 2026

## Current Status

### ✅ Completed This Session (January 3, 2026 - Late Night #2)
1. **My Team Page** - New `/my-teams` page showing user's team roster with current player highlighted
2. **Player Links Menu** - Added My Team and My Schedule links to Player Links dropdown
3. **Smart Season Defaults** - My Team and My Schedule pages prioritize seasons user is registered for
4. **Not Registered Message** - Shows friendly message with register link when viewing unregistered seasons

### 🔧 TODO (From This Session)
- **PDF Export Broken** - `/schedule/{season}/pdf` uses TCPDF which is not installed. Need to either install TCPDF (`composer require tecnickcom/tcpdf`) or implement alternative solution.

### ✅ Completed Previous Session (January 3, 2026 - Late Night)
1. **Unified Visibility Controls** - Schedule Builder buttons now control both per-game published flags AND season-level visibility
2. **Visibility Status Indicator** - Shows current state above action buttons (green=visible, yellow=hidden)
3. **CSS Class Fix** - Changed `visible`/`hidden` to `status-visible`/`status-hidden` to avoid browser conflicts

### ✅ Completed Previous Session (January 3, 2026 - Night)
1. **Intra-Week Team Swap** - Drag team within same week performs atomic swap (no workbench needed)
2. **Reset Button Fix** - "Reset to League Defaults" button no longer disappears after use
3. **Bye Week Date Range Validation** - Validates bye week is within season start/end dates
4. **Bye Week UI Improvements** - Start with 0 inputs, add/remove buttons for each bye week
5. **Visual Feedback** - Green highlight for swap operations, blue for cross-week moves

### ✅ Completed Previous Session (January 3, 2026 - Late Evening)
1. **Masquerade Functionality** - Admin user impersonation using contrib masquerade module
2. **Custom MasqueradeBlock** - Footer block showing form or "masquerading as" state
3. **Admin Restriction Hook** - Prevents masquerading as other administrators
4. **Infrastructure as Code** - Update hook for permission and block placement (no manual steps)
5. **CSRF Token Fix** - Switch back link properly generates CSRF token

### ✅ Completed Previous Session (January 3, 2026 - Evening)
1. **Schedule Grid Unification** - Unified layout across admin Schedule Builder and public Schedule/My Schedule views
2. **ScheduleGridBuilder Service** - Created shared rendering service with three modes (admin/public/my_schedule)
3. **Phase 1: Schedule Builder Display** - Time/Date headers per slot, Field labels with rowspan, home/away rows
4. **Phase 2: Shared Rendering** - Extracted grid logic to service, created shared CSS
5. **Phase 3: Public Schedule Update** - Updated to use shared service, added /my-schedule route
6. **Navigation Persistence** - Fixed position retention when dragging from workbench
7. **Removed "Fill Empty Slots"** - Simplified to only "Generate New Schedule" button

### ✅ Completed Previous Session (January 3, 2026 - Morning)
1. **Teams Page Redesign** - Pill-style season tabs, responsive grid, player sorting, user highlighting
2. **Schedule Page Redesign** - Horizontal grid layout matching schedule builder design
3. **Schedule Navigation** - Prev/Next buttons for week navigation
4. **Pill Tab Caching Fix** - Disabled page caching on Teams and Schedule pages
5. **Future Weeks Padding** - Added 10 weeks of "No games" columns after last game

### ✅ Completed Previous Session (January 2, 2026)
1. **Season Entity Boolean Fields Fix** - Removed `setRequired(TRUE)` from visibility checkboxes
2. **Registration Page Filtering** - Only shows seasons with `registration_visible = TRUE`
3. **Menu Cache Invalidation** - Season changes now take effect immediately
4. **Menu Sort Order Fix** - Seasons display in correct `start_date DESC` order
5. **Season View Page** - Built comprehensive admin view page for seasons
6. **Menu Cleanup** - Removed redundant "Add Season" and "Add Tournament" links
7. **Dev Banner Repositioned** - Moved from page_top to admin toolbar integration

---

## Unified Visibility Controls ✅ (NEW)

### Overview
Schedule Builder "Make Visible to Players" and "Hide from Players" buttons now control both:
- Per-game `published` field (Game entity)
- Season-level `schedule_visible` and `roster_visible` fields (Season entity)

### Files Modified
| File | Change |
|------|--------|
| `ScheduleBuilderForm.php` | Added visibility status indicator, updated publish/unpublish handlers |
| `schedule-builder.css` | Added `.visibility-status`, `.status-visible`, `.status-hidden` classes |

### Key Code in ScheduleBuilderForm.php

**Visibility indicator (buildForm):**
```php
$this->entityTypeManager->getStorage('season')->resetCache([$season_id]);
$fresh_season = $this->entityTypeManager->getStorage('season')->load($season_id);
$is_visible = $fresh_season->get('schedule_visible')->value && $fresh_season->get('roster_visible')->value;
$visibility_class = $is_visible ? 'visibility-status status-visible' : 'visibility-status status-hidden';
$form['visibility_status'] = [
  '#markup' => '<div class="' . $visibility_class . '">' . $visibility_text . '</div>',
  '#weight' => 98,
];
```

**publishSubmit():**
```php
$count = $this->scheduleGenerator->setPublished($season_id, TRUE);
$season->set('schedule_visible', TRUE);
$season->set('roster_visible', TRUE);
$season->save();
```

**unpublishSubmit():**
```php
$count = $this->scheduleGenerator->setPublished($season_id, FALSE);
$season->set('schedule_visible', FALSE);
$season->set('roster_visible', FALSE);
$season->save();
```

---

## Intra-Week Team Swap ✅

### Overview
Allows swapping two teams within the same week with a single drag-and-drop operation, without needing to use the workbench.

### Behavior
- **Same week drag** → Atomic swap (green highlight)
- **Different week drag** → Move with workbench (blue highlight)
- **Same game swap** → Swaps home/away positions
- **Different game swap** → Exchanges teams between games

### Files Created/Modified

| File | Action | Description |
|------|--------|-------------|
| `ccsoccer.routing.yml` | MODIFIED | Added `ccsoccer.schedule_builder_swap` route |
| `src/Controller/ScheduleController.php` | MODIFIED | Added `swapTeams()` endpoint |
| `src/Service/ScheduleGeneratorService.php` | MODIFIED | Added `swapTeamsInWeek()` method |
| `src/Form/ScheduleBuilderForm.php` | MODIFIED | Added `swapUrl` to drupalSettings |
| `js/schedule-builder.js` | MODIFIED | Added swap detection and `handleSwap()` function |
| `css/schedule-builder.css` | MODIFIED | Added `.swap-target` class styling |

### Route Definition
```yaml
ccsoccer.schedule_builder_swap:
  path: '/admin/ccsoccer/season/{season}/schedule/swap'
  defaults:
    _controller: '\Drupal\ccsoccer\Controller\ScheduleController::swapTeams'
  methods: [POST]
  requirements:
    _permission: 'administer ccsoccer'
```

### Key Functions

**`swapTeams()` in ScheduleController.php**
```php
public function swapTeams(Request $request, $season): JsonResponse {
  $data = json_decode($request->getContent(), TRUE);
  
  $source_game_id = $data['source_game_id'] ?? NULL;
  $source_position = $data['source_position'] ?? NULL;
  $target_game_id = $data['target_game_id'] ?? NULL;
  $target_position = $data['target_position'] ?? NULL;

  if (!$source_game_id || !$source_position || !$target_game_id || !$target_position) {
    return new JsonResponse(['success' => FALSE, 'message' => 'Missing required parameters']);
  }

  $result = $this->scheduleGenerator->swapTeamsInWeek(
    $source_game_id, $source_position, $target_game_id, $target_position
  );

  return new JsonResponse($result);
}
```

**`swapTeamsInWeek()` in ScheduleGeneratorService.php**
```php
public function swapTeamsInWeek(int $source_game_id, string $source_position, int $target_game_id, string $target_position): array {
  $source_game = $this->entityTypeManager->getStorage('game')->load($source_game_id);
  $target_game = $this->entityTypeManager->getStorage('game')->load($target_game_id);

  // Validate same week (same game_date)
  if ($source_game->get('game_date')->value !== $target_game->get('game_date')->value) {
    return ['success' => FALSE, 'message' => 'Games must be in the same week'];
  }

  // Get team IDs
  $source_field = $source_position === 'home' ? 'home_team' : 'away_team';
  $target_field = $target_position === 'home' ? 'home_team' : 'away_team';
  $source_team_id = $source_game->get($source_field)->target_id;
  $target_team_id = $target_game->get($target_field)->target_id;

  // Perform atomic swap
  if ($source_game_id === $target_game_id) {
    // Same game: swap home/away
    $source_game->set('home_team', $target_team_id);
    $source_game->set('away_team', $source_team_id);
    $source_game->save();
  } else {
    // Different games: exchange teams
    $source_game->set($source_field, $target_team_id);
    $target_game->set($target_field, $source_team_id);
    $source_game->save();
    $target_game->save();
  }

  return ['success' => TRUE, 'message' => 'Teams swapped successfully'];
}
```

**`handleSwap()` in schedule-builder.js**
```javascript
function handleSwap(config, dragData, targetGameId, targetPosition, state) {
  showToast('Swapping teams...', 'info');

  const requestData = {
    source_game_id: dragData.gameId,
    source_position: dragData.position,
    target_game_id: targetGameId,
    target_position: targetPosition
  };

  fetch(config.swapUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(requestData)
  })
  .then(response => response.json())
  .then(data => {
    if (data.success) {
      showToast(data.message || 'Teams swapped!', 'success');
      sessionStorage.setItem('scheduleBuilderOffset_' + config.seasonId, state.offset.toString());
      location.reload();
    } else {
      showToast(data.message || 'Swap failed', 'error');
    }
  });
}
```

### CSS for Swap Indicator
```css
/* Swap indicator - when dragging within same week */
.game-cell.drag-over.swap-target {
  outline-color: #28a745;
  background: #d4edda !important;
}
```

---

## Reset to League Defaults Fix ✅ (NEW)

### Problem
The "Reset to League Defaults" button disappeared after being clicked once because it was conditionally displayed only when saved config existed in temp store.

### Solution
Always display the button, regardless of whether saved config exists.

### Change in ScheduleBuilderForm.php
```php
// Before: Only show if saved config exists
if ($saved_config) {
  $form['config']['reset_config'] = [...];
}

// After: Always show the button
$form['config']['reset_config'] = [
  '#type' => 'submit',
  '#value' => $this->t('Reset to League Defaults'),
  '#submit' => ['::resetConfigSubmit'],
  '#attributes' => [
    'class' => ['button', 'button--small'],
  ],
  '#limit_validation_errors' => [],
];
```

---

## Bye Week Validation Enhancement ✅ (NEW)

### Overview
Enhanced bye week validation to check if the selected date falls within the season's date range (in addition to existing day-of-week validation).

### Validation Checks
1. **Day of week** - Must match season's day of week (existing)
2. **After start date** - Must not be before season start date (new)
3. **Before end date** - Must not be after season end date (new)

### Example Warnings
- "01/08/2026 is a Thursday, not a Tuesday"
- "01/08/2026 is before the season start date (04/09/2026)"
- "12/25/2026 is after the season end date (11/25/2026)"

### Implementation in schedule-builder.js
```javascript
function initByeWeekValidation(config) {
  const expectedDay = config.dayOfWeek;
  const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

  // Get references to date inputs
  const startDateInput = document.querySelector('input[name="start_date"]');
  const endDateInput = document.querySelector('input[name="end_date"]');
  const daySelect = document.getElementById('edit-day-of-week');

  // Add listeners to bye week inputs, day select, AND start/end date inputs
  document.querySelectorAll('.bye-date-input').forEach(input => {
    input.addEventListener('change', validateByeDates);
  });
  if (daySelect) daySelect.addEventListener('change', validateByeDates);
  if (startDateInput) startDateInput.addEventListener('change', validateByeDates);
  if (endDateInput) endDateInput.addEventListener('change', validateByeDates);

  function formatDateForDisplay(dateStr) {
    if (!dateStr) return '';
    const parts = dateStr.split('-');
    return parts.length === 3 ? `${parts[1]}/${parts[2]}/${parts[0]}` : dateStr;
  }

  function validateByeDates() {
    const currentDay = daySelect ? daySelect.value : expectedDay;
    const startDate = startDateInput ? startDateInput.value : null;
    const endDate = endDateInput ? endDateInput.value : null;
    const warnings = [];

    document.querySelectorAll('.bye-date-input').forEach(input => {
      const dateVal = input.value;
      if (!dateVal) return;

      const date = new Date(dateVal + 'T12:00:00');
      const actualDay = dayNames[date.getDay()];
      const displayDate = formatDateForDisplay(dateVal);

      // Check day of week
      if (actualDay !== currentDay) {
        warnings.push(`${displayDate} is a ${actualDay}, not a ${currentDay}`);
      }

      // Check date range
      if (startDate && dateVal < startDate) {
        warnings.push(`${displayDate} is before the season start date (${formatDateForDisplay(startDate)})`);
      }
      if (endDate && dateVal > endDate) {
        warnings.push(`${displayDate} is after the season end date (${formatDateForDisplay(endDate)})`);
      }
    });

    // Display warnings...
  }
}
```

---

## Bye Week UI Improvements ✅ (NEW)

### Overview
Improved bye weeks section to start empty (most seasons don't have bye weeks) and allow add/remove of individual bye week inputs.

### Changes
1. **Default to 0 bye weeks** - Only "+ Add Bye Week" button shown initially
2. **Remove button for each bye week** - "✕" button next to each date input
3. **Consistent button sizing** - "+ Add Bye Week" same size as "+ Add Time Slot"

### Key Changes in ScheduleBuilderForm.php

**Changed default bye count:**
```php
// Before: Default to 1 empty slot
$bye_count = !empty($saved_bye_weeks) ? count($saved_bye_weeks) : 1;

// After: Default to 0 (most seasons don't have bye weeks)
$bye_count = count($saved_bye_weeks);
```

**Added bye row wrapper with remove button:**
```php
for ($i = 0; $i < $bye_count; $i++) {
  // Wrapper for each bye week row
  $form['config']['bye_weeks_section']['bye_container']['bye_row_' . $i] = [
    '#type' => 'container',
    '#attributes' => ['class' => ['bye-row']],
  ];

  // Date input
  $form['config']['bye_weeks_section']['bye_container']['bye_row_' . $i]['bye_' . $i] = [
    '#type' => 'date',
    // ...
  ];

  // Remove button
  $form['config']['bye_weeks_section']['bye_container']['bye_row_' . $i]['remove_bye_' . $i] = [
    '#type' => 'submit',
    '#value' => $this->t('✕'),
    '#name' => 'remove_bye_' . $i,
    '#submit' => ['::removeByeSubmit'],
    '#ajax' => [
      'callback' => '::ajaxRefreshBye',
      'wrapper' => 'bye-weeks-wrapper',
    ],
    '#limit_validation_errors' => [],
    '#attributes' => [
      'class' => ['remove-bye-btn'],
      'title' => $this->t('Remove this bye week'),
    ],
  ];
}
```

**Added removeByeSubmit handler:**
```php
public function removeByeSubmit(array &$form, FormStateInterface $form_state) {
  $bye_count = $form_state->get('bye_count') ?? 0;
  if ($bye_count > 0) {
    $form_state->set('bye_count', $bye_count - 1);
  }
  $form_state->setRebuild(TRUE);
}
```

### CSS Changes in schedule-builder.css
```css
.bye-container {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  align-items: flex-start;  /* Prevents button from stretching */
}

.bye-row {
  display: flex;
  align-items: flex-end;
  gap: 0.5rem;
}

.bye-row .form-item {
  margin: 0;
}
```

---

## Git Commit Message (This Session)

```
Schedule Builder: Add intra-week team swap, fix reset button, enhance bye weeks

- Add intra-week team swap functionality: dragging a team to another
  position within the same week performs an atomic swap instead of
  requiring the workbench. Visual feedback shows green highlight for
  swaps vs blue for cross-week moves.
- Fix "Reset to League Defaults" button disappearing after use by
  removing conditional display logic
- Enhance bye week validation to check date is within season date range
  (start date to end date), in addition to existing day-of-week check
- Improve bye weeks UI: start with no inputs by default (most seasons
  don't have bye weeks), add remove (x) button to each bye week input
- Format validation warning dates as MM/DD/YYYY for better readability
- Style fixes for consistent button sizing

Files modified:
- ccsoccer.routing.yml (new swap route)
- ScheduleController.php (swapTeams endpoint)
- ScheduleGeneratorService.php (swapTeamsInWeek method)
- ScheduleBuilderForm.php (reset button, bye week UI, swap URL)
- schedule-builder.js (swap handling, bye week date range validation)
- schedule-builder.css (swap indicator, bye week styling)
```

---

## Masquerade Functionality ✅

### Overview
Allows administrators to impersonate non-admin users for testing and player support. Uses the contrib `drupal/masquerade` module with a custom footer block.

### Features
- **Footer block** displays masquerade form (when not masquerading) or "You are masquerading as [user]" with switch back link
- **Admin-only** - Only users with `masquerade as any user` permission see the block
- **Cannot masquerade as other admins** - Custom `hook_masquerade_access()` prevents this
- **CSRF protected** - Switch back link includes proper CSRF token
- **Audit logging** - Masquerade module logs all switch events to watchdog
- **Infrastructure as code** - Permission and block placement via update hook

### Files Created/Modified

| File | Action | Description |
|------|--------|-------------|
| `ccsoccer.module` | MODIFIED | Added `ccsoccer_masquerade_access()` hook + theme hook for block |
| `ccsoccer.libraries.yml` | MODIFIED | Added `masquerade` library |
| `ccsoccer.install` | MODIFIED | Added `ccsoccer_update_9016()` + updated `hook_uninstall()` |
| `src/Plugin/Block/MasqueradeBlock.php` | CREATED | Custom block plugin |
| `templates/ccsoccer-masquerade-block.html.twig` | CREATED | Block template |
| `css/masquerade.css` | CREATED | Dark theme styling for footer |

### Key Functions

**`ccsoccer_masquerade_access()` in ccsoccer.module**
```php
/**
 * Implements hook_masquerade_access().
 *
 * Prevents masquerading as administrator users.
 */
function ccsoccer_masquerade_access($user, $target_account) {
  // Prevent masquerading as administrators (except UID 1 can masquerade as anyone).
  if ($user->id() != 1 && $target_account->hasRole('administrator')) {
    return FALSE;
  }
  // Return NULL to let other access checks decide.
  return NULL;
}
```

**`ccsoccer_update_9016()` in ccsoccer.install**
```php
/**
 * Add masquerade permission to administrator role and place masquerade block in footer.
 */
function ccsoccer_update_9016() {
  // Grant masquerade permission to administrators.
  $role = \Drupal\user\Entity\Role::load('administrator');
  if ($role) {
    $role->grantPermission('masquerade as any user');
    $role->save();
  }

  // Create the masquerade block in footer.
  $existing_block = \Drupal\block\Entity\Block::load('ccsoccer_masquerade');
  if (!$existing_block) {
    $block = \Drupal\block\Entity\Block::create([
      'id' => 'ccsoccer_masquerade',
      'theme' => 'olivero',
      'region' => 'footer_bottom',
      'plugin' => 'ccsoccer_masquerade_block',
      'settings' => [
        'id' => 'ccsoccer_masquerade_block',
        'label' => 'Masquerade',
        'label_display' => '0',
        'provider' => 'ccsoccer',
      ],
      'visibility' => [],
      'weight' => 0,
    ]);
    $block->save();
  }

  return t('Added masquerade permission to administrators and placed block in footer.');
}
```

### Composer Dependency

```bash
ddev composer require drupal/masquerade
ddev drush en masquerade
```

---

## Schedule Grid Unification ✅

### Overview
Unified the schedule grid layout across three views:
- **Admin Schedule Builder** (`/admin/ccsoccer/season/{id}/schedule`) - With drag-drop
- **Public Schedule** (`/schedule`) - View all games, highlights user's team
- **My Schedule** (`/my-schedule`) - Only shows logged-in user's games

### Visual Layout (All Views)
```
┌─────────┬─────────┬─────────┬─────────┬─────────┐
│ 6:00 PM │  JAN 6  │  JAN 13 │  JAN 20 │  JAN 27 │  ← Time/Date header row
├─────────┼─────────┼─────────┼─────────┼─────────┤
│         │ Galaxy  │ Quakes⚽│ Dynamo  │ Rapids  │  ← Home team (pink)
│ Field 1 ├─────────┼─────────┼─────────┼─────────┤
│         │Sounders │  Dash   │  Pride  │Sporting │  ← Away team (white)
├─────────┼─────────┼─────────┼─────────┼─────────┤
│         │ Dynamo  │Red Stars│ Galaxy  │  Dash   │
│ Field 2 ├─────────┼─────────┼─────────┼─────────┤
│         │Redbulls │  Fire   │ Rapids  │Angel City│
├─────────┼─────────┼─────────┼─────────┼─────────┤  ← Dark spacer between slots
│ 7:00 PM │  JAN 6  │  JAN 13 │  JAN 20 │  JAN 27 │  ← Next time slot header
└─────────┴─────────┴─────────┴─────────┴─────────┘
```

---

## ScheduleGridBuilder Service ✅

**File:** `src/Service/ScheduleGridBuilder.php`

### Mode Constants
```php
const MODE_ADMIN = 'admin';       // Drag-drop enabled
const MODE_PUBLIC = 'public';     // View all games, highlight user's team
const MODE_MY_SCHEDULE = 'my_schedule';  // Only user's games shown
```

### Public Methods
| Method | Purpose |
|--------|---------|
| `buildGrid($scheduleState, $allWeeks, $options)` | Main grid HTML generation |
| `calculateAllWeeks($season, $scheduleState)` | Get all weeks including bye/future |
| `calculateCurrentWeekIndex($allWeeks)` | Find current week for auto-scroll |
| `getUserTeamIds($userId)` | Get team IDs for user highlighting |

---

## Routes

### Public Schedule Routes
| Route | Path | Access | Description |
|-------|------|--------|-------------|
| `ccsoccer.schedule_all` | `/schedule` | Public | All games, highlights user's team |
| `ccsoccer.my_schedule_all` | `/my-schedule` | Logged in | Only user's games |
| `ccsoccer.schedule_ical` | `/schedule/{season}/ical` | Public | iCal export |
| `ccsoccer.schedule_pdf` | `/schedule/{season}/pdf` | Public | PDF export |

### Admin Schedule Routes
| Route | Path | Access | Description |
|-------|------|--------|-------------|
| `ccsoccer.schedule_builder` | `/admin/ccsoccer/season/{season}/schedule` | Admin | Schedule Builder form |
| `ccsoccer.schedule_builder_move` | `/admin/ccsoccer/season/{season}/schedule/move` | Admin | Move team endpoint |
| `ccsoccer.schedule_builder_swap` | `/admin/ccsoccer/season/{season}/schedule/swap` | Admin | Swap teams endpoint |

---

## CSS Architecture

### Library Dependencies
```yaml
# Schedule Builder (admin) uses both:
schedule-builder:
  css:
    theme:
      css/schedule-grid.css: {}      # Shared styles
      css/schedule-builder.css: {}   # Admin-only (drag-drop, workbench)

# Public Schedule uses:
schedule-grid:
  css:
    theme:
      css/schedule-grid.css: {}      # Shared styles
  js:
    js/schedule-navigation.js: {}
```

---

## Testing Commands

```bash
# Clear cache after changes
ddev drush cr

# Run database updates
ddev drush updb -y

# Test Schedule Builder
# Visit: /admin/ccsoccer/season/100/schedule
# - Drag team within same week → green highlight, atomic swap
# - Drag team to different week → blue highlight, uses workbench
# - Click "Reset to League Defaults" → resets and button remains visible
# - Click "+ Add Bye Week" → adds date input with remove button
# - Enter bye week outside season dates → warning appears

# Test public Schedule
# Visit: /schedule?season=99
# - Check layout matches admin
# - Check ⚽ icon next to your team

# Test Masquerade
# 1. Log in as administrator
# 2. See masquerade form in footer
# 3. Type a non-admin username, click "Switch"
# 4. Click "Switch back" to return to admin
```

---

## Known Issues / Future Work

### From Previous Sessions (Still Pending)
- **Season-filtered views** - Menu items like View Registrations/Teams/Games go to global collections
- **Tournament menus** - Will add when tournament schedule/roster builder routes exist
- **Additional season actions** - May need Notifications, Reporting, Player Management, Credit Management

### Potential Enhancements
- Add tournament view page similar to season view page
- Add links to filtered registrations/teams/games from season view page
- Add quick stats to season list builder
- Test iCal and PDF export links on schedule page
- Consider adding My Schedule link to user menu

---

## Visibility Flags Behavior Summary

| Flag | Purpose | Where Used |
|------|---------|------------|
| `active` | Season appears in admin menus | `hook_menu_links_discovered_alter()` |
| `registration_visible` | Season appears on public `/register` page | `RegistrationController::available()` |
| `roster_visible` | Players can view team rosters on `/teams` | `ContentController::teamsPage()` |
| `schedule_visible` | Players can view schedule on `/schedule` | `ContentController::schedulePage()` |

---

## Session Continuity

### Current Architecture Patterns
- Public pages in `ContentController.php`
- Shared rendering in `ScheduleGridBuilder` service
- Entity view pages in entity-specific controllers (e.g., `SeasonController.php`)
- Global CSS via `content-pages` library
- Shared schedule CSS via `schedule-grid` library
- Admin-specific schedule CSS via `schedule-builder` library
- Entity-specific CSS via dedicated libraries (e.g., `season-view`)
- Dynamic admin menus via hook in `.module`
- Cache invalidation on entity save for menu updates
- Drupal behaviors with `once()` for JavaScript initialization
- `max-age => 0` for pages with dynamic query parameters
- `sessionStorage` for cross-reload state persistence
- **Infrastructure as code** - Permissions and block placement via update hooks (no manual admin steps)
- **Contrib module integration** - Using masquerade module with custom block wrapper

### For Next Session
- Consider building tournament view page
- May want to add season-specific links to registrations/teams/games
- Continue with registration/tournament features as needed
- Test iCal/PDF export functionality
- Consider adding My Schedule to user menu

---

**End of Session - Schedule Builder Enhancements Complete!**
