Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/components/health_reports/report_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ See COPYRIGHT and LICENSE files for more details.

result_group.results.each do |value|
box.with_row do
render(HealthReports::ResultComponent.new(group: result_group.key, result: value, i18n_scope:))
render(HealthReports::ResultComponent.new(group: result_group.key, result: value, i18n_scope:, docs_href:))
end
end
end
Expand Down
8 changes: 6 additions & 2 deletions app/components/health_reports/report_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,18 @@ class ReportComponent < ApplicationComponent

# The i18n_scope parameter defines the I18n scope that should be used to resolve
# names of groups, checks and error messages indicated by the results.
def initialize(*, i18n_scope:, **)
#
# docs_href overrides where each result's "More information" link points;
# without it, results link to the file storages troubleshooting page.
def initialize(*, i18n_scope:, docs_href: nil, **)
super(*, **)
@i18n_scope = i18n_scope
@docs_href = docs_href
end

private

attr_reader :i18n_scope
attr_reader :i18n_scope, :docs_href

def summary_scheme(check_tally)
case check_tally
Expand Down
12 changes: 7 additions & 5 deletions app/components/health_reports/result_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,20 @@ See COPYRIGHT and LICENSE files for more details.
line.with_column(mr: 2) do
render(Primer::Beta::Text.new(font_size: :small, color: status_color)) { status_text }
end
if error_code.present?
if error_code.present? && docs_href.present?
line.with_column do
render(Primer::Beta::Label.new(scheme: status_color)) { error_code }
end
end
end

if error_code.present?
if error_code.present? && docs_href.present?
row.with_column do
helpers.static_link_to(href: docs_href,
label: I18n.t(:label_more_information),
underline: true)
helpers.static_link_to(
href: docs_href,
label: I18n.t(:label_more_information),
underline: true
)
end
end
end
Expand Down
16 changes: 14 additions & 2 deletions app/components/health_reports/result_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,18 @@ module HealthReports
class ResultComponent < ApplicationComponent
include OpPrimer::ComponentHelpers

def initialize(group:, result:, i18n_scope:)
# Where "More information" points. Defaults to the file storages
# documentation because that was this component's only consumer for a long
# time; a subject with its own troubleshooting page passes its own, and one
# with no page yet passes false to suppress the link rather than send an
# administrator somewhere unrelated.
DEFAULT_DOCS_HREF = -> { ::OpenProject::Static::Links.url_for(:storage_docs, :health_status) }

def initialize(group:, result:, i18n_scope:, docs_href: nil)
super(result)
@group = group
@i18n_scope = i18n_scope
@docs_href = docs_href
end

private
Expand All @@ -49,7 +57,11 @@ def error_text
I18n.t("errors.#{model.code}", scope: @i18n_scope, **model.context&.symbolize_keys)
end

def docs_href = ::OpenProject::Static::Links.url_for(:storage_docs, :health_status)
def docs_href
return @docs_href if @docs_href == false

@docs_href || DEFAULT_DOCS_HREF.call
end

def error_code
if model.failure?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<%=
component_wrapper(tag: :turbo_frame, refresh: :morph) do
render(Primer::OpenProject::SidePanel::Section.new) do |section|
section.with_title(tag: :h3) { t(".title") }

flex_layout do |container|
if report.present?
container.with_row do
header = summary_header
concat(render(Primer::Beta::Octicon.new(icon: header[:icon], color: header[:icon_color], mr: 2)))
concat(render(Primer::Beta::Text.new(font_weight: :bold)) { header[:text] })
end

container.with_row(mt: 2) do
render(
Primer::Beta::Text.new(color: :muted, test_selector: "llm-connection--health-summary")
) { summary_description }
end

container.with_row(mt: 2) do
render(
Primer::Beta::Button.new(
scheme: :link, color: :default, font_weight: :bold,
tag: :a, href: llm_connection_health_status_report_path,
test_selector: "llm-connection--open-health-report"
)
) do |button|
button.with_leading_visual_icon(icon: :meter)
t(".open_report")
end
end
else
container.with_row do
render(Primer::Beta::Text.new(color: :muted)) { t(".never_checked") }
end

container.with_row(mt: 2) do
primer_form_with(
url: create_health_status_report_llm_connection_health_status_report_path,
method: :post,
data: { turbo: true }
) do
render(
Primer::Beta::Button.new(
scheme: :link, color: :default, font_weight: :bold, type: :submit,
test_selector: "llm-connection--run-health-checks"
)
) do |button|
button.with_leading_visual_icon(icon: :meter)
t(".run_checks")
end
end
end
end
end
end
end
%>
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module LlmConnections
module SidePanel
# "Has this connection ever actually worked?", answered on the settings page.
class HealthStatusComponent < ApplicationComponent
include ApplicationHelper
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers

alias_method :connection, :model

private

def report
connection.latest_health_report
end

def summary_header
tally = report.tally

case tally
in { failure: 1.. }
{ icon: :alert, icon_color: :danger,
text: I18n.t("health_reports.common.checks.failures", count: tally[:failure]) }
in { warning: 1.. }
{ icon: :alert, icon_color: :attention,
text: I18n.t("health_reports.common.checks.warnings", count: tally[:warning]) }
else
{ icon: :"check-circle", icon_color: :success, text: I18n.t("health_reports.common.checks.success") }
end
end

def summary_description
text = if report.healthy?
I18n.t("health_reports.common.summary.success")
elsif report.unhealthy?
I18n.t("health_reports.common.summary.failure")
else
I18n.t("health_reports.common.summary.warning")
end

"#{text} #{t('.last_check', datetime: helpers.format_time(report.created_at))}"
end
end
end
end
1 change: 1 addition & 0 deletions app/controllers/admin/llm_connections_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def disconnect
return redirect_with_error(t(".configured_from_env")) if @connection.configured_from_env?

@connection.update!(api_key: nil, enabled: false)
Llm::HealthCheckJob.toggle_cron_job

redirect_with_notice(t(".success"))
end
Expand Down
112 changes: 112 additions & 0 deletions app/controllers/admin/llm_health_status_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Admin
class LlmHealthStatusController < ApplicationController
include OpTurbo::ComponentStream

layout :admin_or_frame_layout

before_action :require_feature
before_action :require_admin
before_action :find_connection

menu_item :llm_connection

def show
@report = @connection.latest_health_report

respond_to do |format|
format.html
format.text do
return head :not_found if @report.nil?

timestamp = @report.created_at.iso8601
send_data text_report(timestamp),
filename: "llm_connection_health_report_#{timestamp}.txt",
type: "text/plain",
disposition: :attachment
end
end
end

# A full run, including the billed completion: an administrator clicking
# "Run checks" is asking whether the connection actually works.
def create
run_checks
redirect_to llm_connection_health_status_report_path, status: :see_other
end

def create_health_status_report
run_checks
update_via_turbo_stream(component: LlmConnections::SidePanel::HealthStatusComponent.new(@connection))
respond_with_turbo_streams
end

private

def admin_or_frame_layout
turbo_frame_request? ? "turbo_rails/frame" : "admin"
end

def run_checks
@connection.deep_health_check = true
report = Llm::Validators::ConnectionValidator.new(@connection).call
report.save!
report
end

# Downloaded and pasted into support tickets, so it must not contain the API
# key or the custom headers -- a gateway header routinely carries a second
# credential.
def text_report(timestamp)
{
connection: @connection.name,
configuration: @connection.non_confidential_configuration,
ran_at: timestamp,
results: @report ? @report.results.map(&:to_h) : []
}.to_yaml(stringify_names: true)
end

# HealthReports::Validator builds the report through the association, which
# would insert an unpersisted singleton along with it.
def find_connection
@connection = LlmConnection.instance

redirect_to llm_connection_path unless @connection.persisted?
end

# The flag gates the endpoints, not only the menu entry: an unfinished page
# must not accept writes just because somebody knows the URL.
def require_feature
render_404 unless OpenProject::FeatureDecisions.llm_connection_active?
end
end
end
Loading
Loading