Files
cachet/IMPLEMENTATION_DETAILS.md
2026-01-08 15:32:23 +00:00

7.0 KiB

Sticky Incidents Fix - Code Changes Summary

Overview

This document provides a detailed breakdown of the code changes made to fix the sticky incidents display issue.

Problem Statement

From the GitHub issue:

  • User created an incident and marked it as "sticky"
  • Expected: Incident should display prominently at the top of the status page
  • Actual: Incident only appeared in the "Past Incidents" section under its date (1/2/2026)

Solution Architecture

1. Data Layer Changes (IncidentTimeline.php)

New Method: stickiedIncidents()

Fetches all sticky incidents that should be displayed at the top:

private function stickiedIncidents(): Collection
{
    return Incident::query()
        ->with([
            'components',
            'updates' => fn ($query) => $query->orderByDesc('created_at'),
        ])
        ->visible(auth()->check())
        ->stickied()  // Uses existing scope from Incident model
        ->get()
        ->sortByDesc(fn (Incident $incident) => $incident->timestamp);
}

Key Points:

  • Uses the existing stickied() scope defined in the Incident model
  • Maintains visibility checks (respects authentication status)
  • Eager loads components and updates for performance
  • Sorts by timestamp (newest first)

Modified Method: incidents()

Excludes sticky incidents from the regular timeline:

private function incidents(Carbon $startDate, Carbon $endDate, bool $onlyDisruptedDays = false): Collection
{
    return Incident::query()
        ->with([...])
        ->visible(auth()->check())
        ->where('stickied', false)  // ← NEW: Exclude sticky incidents
        ->when($this->appSettings->recent_incidents_only, function ($query) {
            // ... existing logic
        })
        // ... rest of the method
}

Key Points:

  • Added ->where('stickied', false) to explicitly exclude sticky incidents
  • This ensures sticky incidents don't appear in both sections
  • All other logic remains unchanged

Updated Method: render()

Passes sticky incidents to the view:

public function render(): View
{
    // ... existing logic
    
    return view('cachet::components.incident-timeline', [
        'stickiedIncidents' => $this->stickiedIncidents(),  // ← NEW
        'incidents' => $this->incidents(...),
        // ... existing data
    ]);
}

2. Presentation Layer Changes (incident-timeline.blade.php)

New Section: Stickied Incidents

Added at the top of the timeline, before "Past Incidents":

@if($stickiedIncidents->isNotEmpty())
<div class="flex flex-col gap-5">
    <div class="md:border-b py-2 dark:border-zinc-700">
        <h2 class="text-2xl font-semibold">
            {{ __('cachet::incident.timeline.stickied_incidents_header') }}
        </h2>
    </div>
    
    @foreach($stickiedIncidents as $incident)
    <div class="ring-2 ring-amber-500 dark:ring-amber-600 ...">
        <div class="bg-amber-50 dark:bg-amber-950/30 ...">
            <!-- Incident details -->
        </div>
        <!-- Updates section -->
    </div>
    @endforeach
</div>
@endif

Visual Styling:

  • ring-2 ring-amber-500 dark:ring-amber-600 - Prominent amber border
  • bg-amber-50 dark:bg-amber-950/30 - Subtle amber background tint
  • Maintains all existing incident display features (updates, status, components)

Conditional Display:

  • Section only appears if $stickiedIncidents->isNotEmpty()
  • No visual clutter when no sticky incidents exist

3. Localization Changes (incident.php)

Added translation key for the new section header:

'timeline' => [
    'past_incidents_header' => 'Past Incidents',
    'recent_incidents_header' => 'Recent Incidents',
    'stickied_incidents_header' => 'Stickied Incidents',  // ← NEW
    // ...
],

4. Test Coverage (IncidentTimelineTest.php)

Test 1: Display Separation

it('displays stickied incidents separately at the top', function () {
    $stickyIncident = Incident::factory()->create(['stickied' => true]);
    $regularIncident = Incident::factory()->create(['stickied' => false]);
    
    $response = get(route('cachet.status-page'));
    
    $response->assertSeeInOrder([
        'Stickied Incidents',
        'Important Sticky Incident',
        'Past Incidents',
        'Regular Incident',
    ]);
});

Test 2: No Duplication

it('does not show stickied incidents in the regular timeline', function () {
    // Creates both sticky and regular incidents
    // Verifies sticky incidents only in stickiedIncidents collection
    // Verifies regular incidents only in incidents collection
});

Test 3: Sorting

it('shows multiple stickied incidents sorted by timestamp', function () {
    // Verifies newer sticky incidents appear first
});

Test 4: Conditional Display

it('does not show stickied incidents section when there are none', function () {
    // Verifies section doesn't appear unnecessarily
});

Data Flow Diagram

User requests status page
         ↓
IncidentTimeline component loads
         ↓
    ┌────┴────┐
    ↓         ↓
stickiedIncidents()   incidents()
    ↓                  ↓
Query: stickied=true   Query: stickied=false
    ↓                  ↓
Sorted by timestamp    Grouped by date
    ↓                  ↓
    └────┬────┘
         ↓
Passed to view template
         ↓
    ┌────┴────┐
    ↓         ↓
Sticky Section   Timeline Section
(if not empty)   (date-grouped)
    ↓                ↓
[Amber Border]   [Regular Display]

Performance Considerations

Optimizations Implemented:

  1. Single Query per Type: One query for sticky incidents, one for regular
  2. Eager Loading: Components and updates loaded upfront
  3. Conditional Rendering: Sticky section only renders when needed
  4. Existing Scopes: Leverages existing stickied() scope on Incident model

Database Impact:

  • Before: 1 query for all incidents
  • After: 2 queries (1 for sticky, 1 for regular)
  • Trade-off: Minimal - Better UX worth the extra query

Backward Compatibility

Fully backward compatible:

  • Existing incidents without stickied=true display unchanged
  • No database migrations required (column already exists)
  • No API changes
  • Existing tests remain valid

Edge Cases Handled

  1. No sticky incidents → Section doesn't display
  2. Multiple sticky incidents → Sorted by timestamp
  3. Sticky incident with updates → All updates display correctly
  4. Visibility rules → Respects auth()->check() for both sections
  5. Date filtering → Sticky incidents always show (not date-filtered)

Migration Path

For deployment to production:

  1. Merge changes to cachethq/core repository
  2. Tag new version of core package
  3. Update cachethq/cachet to use new core version
  4. No database migrations needed
  5. No configuration changes required

Rollback Plan

If issues arise:

  1. Revert core package to previous version
  2. No data loss (database unchanged)
  3. System returns to previous behavior