mirror of
https://github.com/cachethq/cachet.git
synced 2026-03-03 02:17:01 +00:00
7.0 KiB
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 borderbg-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:
- Single Query per Type: One query for sticky incidents, one for regular
- Eager Loading: Components and updates loaded upfront
- Conditional Rendering: Sticky section only renders when needed
- 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=truedisplay unchanged - No database migrations required (column already exists)
- No API changes
- Existing tests remain valid
Edge Cases Handled
- ✅ No sticky incidents → Section doesn't display
- ✅ Multiple sticky incidents → Sorted by timestamp
- ✅ Sticky incident with updates → All updates display correctly
- ✅ Visibility rules → Respects auth()->check() for both sections
- ✅ Date filtering → Sticky incidents always show (not date-filtered)
Migration Path
For deployment to production:
- Merge changes to
cachethq/corerepository - Tag new version of core package
- Update
cachethq/cachetto use new core version - No database migrations needed
- No configuration changes required
Rollback Plan
If issues arise:
- Revert core package to previous version
- No data loss (database unchanged)
- System returns to previous behavior