discourse/app/models/problem_check_tracker.rb
Ted Johansson 623f2279b0
FEATURE: Add problem checks page to admin panel and allow ignoring problem checks (#39103)
## Background

Currently you can "dismiss" problem checks on the dashboard, but if the
problem persists it will show up again on the next reload, which is
confusing.

There was previously some discussion about adding a feature to "snooze"
problem checks, but I think even with that it remains a bit too opaque.
You'd need to spelunk around in the console to try and figure out what
is going on.

## What is this change?

This PR does a couple of things:

### 1. Replace Dismiss with Ignore

Hitting ignore will prevent the problem check from creating new admin
notices until it has been unignored from the new problem checks page.

**Screenshot**

<img width="395" height="61" alt="Screenshot 2026-04-05 at 4 37 39 PM"
src="https://github.com/user-attachments/assets/4816fd04-046b-441e-9471-c160dd3f82b9"
/>

### 2. Add a new problem check page

This page provides a list of problem checks with information on whether
they are passing or failing, and when they were last run. You can also
ignore or unignore (watch) problem checks from here.

**Screenshot**

<img width="600" height="200" alt="Screenshot 2026-04-05 at 4 26 37 PM"
src="https://github.com/user-attachments/assets/d8cb2b6a-3f56-409c-97f0-312cb1545654"
/>

### 3. Remove the problem check timestamp from the dashboard

This timestamp made sense under the previous model, where all checks
were run at once and the results cached. With the new model, there's a
mix of on-demand and scheduled checks, and having a single timestamp is
misleading at best. In practice it's always going to be just the
timestamp when you last loaded the dashboard.

**Before**

<img width="240" height="100" alt="Screenshot 2026-04-05 at 4 30 21 PM"
src="https://github.com/user-attachments/assets/1846e024-0042-476e-8b5d-41b6745af75f"
/>

**After**

<img width="210" height="95" alt="Screenshot 2026-04-05 at 4 29 19 PM"
src="https://github.com/user-attachments/assets/a39c87c1-c1e3-4621-8219-e3903ba2ada4"
/>
2026-04-23 08:28:33 +08:00

129 lines
2.8 KiB
Ruby

# frozen_string_literal: true
class ProblemCheckTracker < ActiveRecord::Base
validates :identifier, presence: true, uniqueness: { scope: :target }
validates :target, presence: true
validates :blips, presence: true, numericality: { greater_than_or_equal_to: 0 }
scope :failing, -> { where("last_problem_at = last_run_at") }
scope :passing, -> { where("last_success_at = last_run_at") }
scope :ignored, -> { where.not(ignored_at: nil) }
scope :watched, -> { where(ignored_at: nil) }
before_destroy :silence_the_alarm
def self.[](identifier, target = ProblemCheck::NO_TARGET)
find_or_create_by(identifier:, target:)
end
def ready_to_run?
next_run_at.blank? || next_run_at.past?
end
def failing?
last_problem_at == last_run_at
end
def passing?
last_success_at == last_run_at
end
def ignored?
ignored_at.present?
end
def watched?
!ignored?
end
def ignore!
return if ignored?
touch(:ignored_at)
silence_the_alarm
end
def watch!
return if watched?
update!(ignored_at: nil)
sound_the_alarm if sound_the_alarm?
end
def problem!(next_run_at: nil, details: {})
now = Time.current
update!(blips: blips + 1, details:, last_run_at: now, last_problem_at: now, next_run_at:)
update_notice_details(details)
sound_the_alarm if sound_the_alarm?
end
def no_problem!(next_run_at: nil)
reset(next_run_at:)
silence_the_alarm
end
def check
check = ProblemCheck[identifier]
return check if check.present?
silence_the_alarm
destroy
nil
end
private
def reset(next_run_at: nil)
now = Time.current
update!(blips: 0, last_run_at: now, last_success_at: now, next_run_at:)
end
def update_notice_details(details)
admin_notice.where(identifier:).update_all(details: details.merge(target:))
end
def sound_the_alarm?
watched? && failing? && blips > check.max_blips
end
def sound_the_alarm
admin_notice.create_with(
priority: check.priority,
details: details.merge(target:),
).find_or_create_by(identifier:)
end
def silence_the_alarm
admin_notice.where(identifier:).delete_all
end
def admin_notice
AdminNotice.problem.where("details->>'target' = ?", target)
end
end
# == Schema Information
#
# Table name: problem_check_trackers
#
# id :bigint not null, primary key
# blips :integer default(0), not null
# details :json
# identifier :string not null
# ignored_at :datetime
# last_problem_at :datetime
# last_run_at :datetime
# last_success_at :datetime
# next_run_at :datetime
# target :string default("__NULL__"), not null
#
# Indexes
#
# index_problem_check_trackers_on_identifier_and_target (identifier,target) UNIQUE
#