telegram bot logs, first pass
This commit is contained in:
108
app/controllers/telegram_bot_logs_controller.rb
Normal file
108
app/controllers/telegram_bot_logs_controller.rb
Normal file
@@ -0,0 +1,108 @@
|
||||
# typed: false
|
||||
class TelegramBotLogsController < ApplicationController
|
||||
before_action :set_telegram_bot_log, only: %i[show]
|
||||
after_action :verify_authorized
|
||||
|
||||
def index
|
||||
authorize TelegramBotLog
|
||||
|
||||
# Start with policy scope
|
||||
@telegram_bot_logs = policy_scope(TelegramBotLog)
|
||||
|
||||
# Apply filters
|
||||
@telegram_bot_logs = apply_filters(@telegram_bot_logs)
|
||||
|
||||
# Order by most recent first
|
||||
@telegram_bot_logs = @telegram_bot_logs.recent
|
||||
|
||||
# Paginate with Kaminari
|
||||
@limit = (params[:limit] || 50).to_i.clamp(1, 500)
|
||||
@telegram_bot_logs = @telegram_bot_logs.page(params[:page]).per(@limit)
|
||||
|
||||
# Load associations for display
|
||||
@telegram_bot_logs = @telegram_bot_logs.includes(:processed_image)
|
||||
|
||||
# Set up filter options for the view
|
||||
@status_options = TelegramBotLog.statuses.keys
|
||||
@filter_params =
|
||||
params.slice(
|
||||
:telegram_user_id,
|
||||
:status,
|
||||
:start_date,
|
||||
:end_date,
|
||||
:min_results,
|
||||
:max_results,
|
||||
:slow_requests,
|
||||
)
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @telegram_bot_log
|
||||
|
||||
# The processed_image association will be loaded automatically when accessed
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_telegram_bot_log
|
||||
@telegram_bot_log =
|
||||
TelegramBotLog.includes(:processed_image).find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to telegram_bot_logs_path, alert: "Telegram bot log not found."
|
||||
end
|
||||
|
||||
def apply_filters(scope)
|
||||
# Filter by telegram user ID
|
||||
if params[:telegram_user_id].present?
|
||||
scope = scope.for_user(params[:telegram_user_id].to_i)
|
||||
end
|
||||
|
||||
# Filter by status
|
||||
if params[:status].present? && TelegramBotLog.statuses.key?(params[:status])
|
||||
scope = scope.where(status: params[:status])
|
||||
end
|
||||
|
||||
# Filter by date range
|
||||
if params[:start_date].present?
|
||||
begin
|
||||
start_date = Date.parse(params[:start_date])
|
||||
scope =
|
||||
scope.where("request_timestamp >= ?", start_date.beginning_of_day)
|
||||
rescue Date::Error
|
||||
# Ignore invalid date
|
||||
end
|
||||
end
|
||||
|
||||
if params[:end_date].present?
|
||||
begin
|
||||
end_date = Date.parse(params[:end_date])
|
||||
scope = scope.where("request_timestamp <= ?", end_date.end_of_day)
|
||||
rescue Date::Error
|
||||
# Ignore invalid date
|
||||
end
|
||||
end
|
||||
|
||||
# Filter by search results count
|
||||
if params[:min_results].present?
|
||||
scope =
|
||||
scope.where("search_results_count >= ?", params[:min_results].to_i)
|
||||
end
|
||||
|
||||
if params[:max_results].present?
|
||||
scope =
|
||||
scope.where("search_results_count <= ?", params[:max_results].to_i)
|
||||
end
|
||||
|
||||
# Filter by performance metrics
|
||||
if params[:slow_requests].present? && params[:slow_requests] == "true"
|
||||
# Consider requests with total processing time > 1 second as slow
|
||||
scope =
|
||||
scope.where(
|
||||
"(fingerprint_computation_time + search_computation_time) > ? OR fingerprint_computation_time IS NULL OR search_computation_time IS NULL",
|
||||
1.0,
|
||||
)
|
||||
end
|
||||
|
||||
scope
|
||||
end
|
||||
end
|
||||
35
app/lib/stopwatch.rb
Normal file
35
app/lib/stopwatch.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
# typed: strict
|
||||
# A simple stopwatch utility for measuring elapsed time
|
||||
class Stopwatch
|
||||
extend T::Sig
|
||||
|
||||
sig { params(start_time: T.any(Time, ActiveSupport::TimeWithZone)).void }
|
||||
def initialize(start_time)
|
||||
@start_time = T.let(start_time, T.any(Time, ActiveSupport::TimeWithZone))
|
||||
end
|
||||
|
||||
sig { returns(Stopwatch) }
|
||||
def self.start
|
||||
new(Time.current)
|
||||
end
|
||||
|
||||
sig { returns(Float) }
|
||||
def elapsed
|
||||
Time.current - @start_time
|
||||
end
|
||||
|
||||
sig { returns(String) }
|
||||
def elapsed_ms
|
||||
"#{(elapsed * 1000).round(1)}ms"
|
||||
end
|
||||
|
||||
sig { returns(String) }
|
||||
def elapsed_s
|
||||
"#{sprintf("%.3f", elapsed)}s"
|
||||
end
|
||||
|
||||
sig { returns(T.any(Time, ActiveSupport::TimeWithZone)) }
|
||||
def start_time
|
||||
@start_time
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,7 @@ require "telegram/bot"
|
||||
require "tempfile"
|
||||
require "net/http"
|
||||
require "uri"
|
||||
require_relative "../stopwatch"
|
||||
|
||||
module Tasks
|
||||
class TelegramBotTask
|
||||
@@ -76,8 +77,14 @@ module Tasks
|
||||
def handle_message(bot, message)
|
||||
return unless message.photo || message.document
|
||||
|
||||
# Start timing the total request
|
||||
total_request_timer = Stopwatch.start
|
||||
|
||||
chat_id = message.chat.id
|
||||
|
||||
# Create initial log record
|
||||
telegram_log = create_telegram_log(message)
|
||||
|
||||
# Send initial response
|
||||
response_message =
|
||||
bot.api.send_message(
|
||||
@@ -88,13 +95,34 @@ module Tasks
|
||||
|
||||
begin
|
||||
# Process the image and perform visual search
|
||||
search_result = process_image_message(bot, message)
|
||||
search_result, processed_blob =
|
||||
process_image_message_with_logging(bot, message, telegram_log)
|
||||
|
||||
if search_result
|
||||
if search_result && !search_result.empty?
|
||||
result_text = format_search_results(search_result)
|
||||
|
||||
# Update log with success
|
||||
update_telegram_log_success(
|
||||
telegram_log,
|
||||
search_result,
|
||||
result_text,
|
||||
processed_blob,
|
||||
)
|
||||
elsif search_result
|
||||
result_text = "❌ No close matches found."
|
||||
|
||||
# Update log with no results
|
||||
update_telegram_log_no_results(
|
||||
telegram_log,
|
||||
result_text,
|
||||
processed_blob,
|
||||
)
|
||||
else
|
||||
result_text =
|
||||
"❌ Could not process the image. Please make sure it's a valid image file."
|
||||
|
||||
# Update log with invalid image
|
||||
update_telegram_log_invalid_image(telegram_log, result_text)
|
||||
end
|
||||
|
||||
# Update the response with results
|
||||
@@ -104,9 +132,17 @@ module Tasks
|
||||
text: result_text,
|
||||
parse_mode: "Markdown",
|
||||
)
|
||||
|
||||
# Record total request time
|
||||
total_request_time = total_request_timer.elapsed
|
||||
telegram_log.update!(total_request_time: total_request_time)
|
||||
log("⏱️ Total request completed in #{total_request_timer.elapsed_s}")
|
||||
rescue StandardError => e
|
||||
log("Error processing image: #{e.message}")
|
||||
|
||||
# Update log with error
|
||||
update_telegram_log_error(telegram_log, e)
|
||||
|
||||
# Update with error message
|
||||
bot.api.edit_message_text(
|
||||
chat_id: chat_id,
|
||||
@@ -114,6 +150,13 @@ module Tasks
|
||||
text:
|
||||
"❌ An error occurred while processing your image. Please try again.",
|
||||
)
|
||||
|
||||
# Record total request time even for errors
|
||||
total_request_time = total_request_timer.elapsed
|
||||
telegram_log.update!(total_request_time: total_request_time)
|
||||
log(
|
||||
"⏱️ Total request (with error) completed in #{total_request_timer.elapsed_s}",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -121,34 +164,76 @@ module Tasks
|
||||
params(
|
||||
bot: Telegram::Bot::Client,
|
||||
message: Telegram::Bot::Types::Message,
|
||||
telegram_log: TelegramBotLog,
|
||||
).returns(
|
||||
T.nilable(
|
||||
T::Array[Domain::VisualSearchHelper::SimilarFingerprintResult],
|
||||
),
|
||||
[
|
||||
T.nilable(
|
||||
T::Array[Domain::VisualSearchHelper::SimilarFingerprintResult],
|
||||
),
|
||||
T.nilable(BlobFile),
|
||||
],
|
||||
)
|
||||
end
|
||||
def process_image_message(bot, message)
|
||||
def process_image_message_with_logging(bot, message, telegram_log)
|
||||
log("📥 Received image message from chat #{message.chat.id}")
|
||||
|
||||
# Get the largest photo or document
|
||||
image_file = get_image_file_from_message(message)
|
||||
return nil unless image_file
|
||||
return nil, nil unless image_file
|
||||
|
||||
# Download the image to a temporary file
|
||||
download_stopwatch = Stopwatch.start
|
||||
temp_file = download_telegram_image(bot, image_file)
|
||||
return nil unless temp_file
|
||||
download_time = download_stopwatch.elapsed
|
||||
|
||||
return nil, nil unless temp_file
|
||||
|
||||
log("📥 Downloaded image in #{download_stopwatch.elapsed_s}")
|
||||
|
||||
processed_blob = nil
|
||||
|
||||
begin
|
||||
# Generate fingerprints
|
||||
# Time image processing (file reading and BlobFile creation)
|
||||
image_processing_stopwatch = Stopwatch.start
|
||||
|
||||
file_path = T.must(temp_file.path)
|
||||
file_content = File.binread(file_path)
|
||||
|
||||
# Create BlobFile for the processed image
|
||||
content_type =
|
||||
case image_file
|
||||
when Telegram::Bot::Types::Document
|
||||
image_file.mime_type || "application/octet-stream"
|
||||
when Telegram::Bot::Types::PhotoSize
|
||||
"image/jpeg" # Telegram photos are typically JPEG
|
||||
else
|
||||
"application/octet-stream"
|
||||
end
|
||||
|
||||
processed_blob =
|
||||
BlobFile.find_or_initialize_from_contents(file_content) do |blob|
|
||||
blob.content_type = content_type
|
||||
end
|
||||
processed_blob.save! unless processed_blob.persisted?
|
||||
|
||||
image_processing_time = image_processing_stopwatch.elapsed
|
||||
|
||||
log("🔧 Processed image in #{image_processing_stopwatch.elapsed_s}")
|
||||
|
||||
# Time fingerprint generation
|
||||
fingerprint_stopwatch = Stopwatch.start
|
||||
fingerprint_value =
|
||||
Domain::PostFile::BitFingerprint.from_file_path(file_path)
|
||||
detail_fingerprint_value =
|
||||
Domain::PostFile::BitFingerprint.detail_from_file_path(file_path)
|
||||
fingerprint_computation_time = fingerprint_stopwatch.elapsed
|
||||
|
||||
log("🔍 Generated fingerprints, searching for similar images...")
|
||||
log(
|
||||
"🔍 Generated fingerprints in #{fingerprint_stopwatch.elapsed_s}, searching for similar images...",
|
||||
)
|
||||
|
||||
# Find similar fingerprints using the existing helper
|
||||
# Time search operation
|
||||
search_stopwatch = Stopwatch.start
|
||||
similar_results =
|
||||
find_similar_fingerprints(
|
||||
fingerprint_value: fingerprint_value,
|
||||
@@ -159,24 +244,144 @@ module Tasks
|
||||
post_file: :post,
|
||||
},
|
||||
)
|
||||
search_computation_time = search_stopwatch.elapsed
|
||||
|
||||
# Update timing metrics in log
|
||||
telegram_log.update!(
|
||||
download_time: download_time,
|
||||
image_processing_time: image_processing_time,
|
||||
fingerprint_computation_time: fingerprint_computation_time,
|
||||
search_computation_time: search_computation_time,
|
||||
)
|
||||
|
||||
# Filter to only >90% similarity
|
||||
high_quality_matches =
|
||||
similar_results.select { |result| result.similarity_percentage > 90 }
|
||||
|
||||
log(
|
||||
"✅ Found #{high_quality_matches.length} high-quality matches (>90% similarity)",
|
||||
"✅ Found #{high_quality_matches.length} high-quality matches (>90% similarity) in #{search_stopwatch.elapsed_s}",
|
||||
)
|
||||
high_quality_matches
|
||||
|
||||
[high_quality_matches, processed_blob]
|
||||
rescue StandardError => e
|
||||
log("❌ Error processing image: #{e.message}")
|
||||
nil
|
||||
[nil, processed_blob]
|
||||
ensure
|
||||
# Clean up temp file
|
||||
temp_file.unlink if temp_file
|
||||
end
|
||||
end
|
||||
|
||||
# Logging helper methods
|
||||
sig do
|
||||
params(message: Telegram::Bot::Types::Message).returns(TelegramBotLog)
|
||||
end
|
||||
def create_telegram_log(message)
|
||||
user = message.from
|
||||
chat = message.chat
|
||||
|
||||
TelegramBotLog.create!(
|
||||
telegram_user_id: user&.id || 0,
|
||||
telegram_username: user&.username,
|
||||
telegram_first_name: user&.first_name,
|
||||
telegram_last_name: user&.last_name,
|
||||
telegram_chat_id: chat.id,
|
||||
request_timestamp: Time.current,
|
||||
status: :success, # Will be updated later
|
||||
search_results_count: 0,
|
||||
response_data: {
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
telegram_log: TelegramBotLog,
|
||||
search_results:
|
||||
T::Array[Domain::VisualSearchHelper::SimilarFingerprintResult],
|
||||
response_text: String,
|
||||
processed_blob: T.nilable(BlobFile),
|
||||
).void
|
||||
end
|
||||
def update_telegram_log_success(
|
||||
telegram_log,
|
||||
search_results,
|
||||
response_text,
|
||||
processed_blob
|
||||
)
|
||||
telegram_log.update!(
|
||||
status: :success,
|
||||
search_results_count: search_results.length,
|
||||
processed_image: processed_blob,
|
||||
response_data: {
|
||||
response_text: response_text,
|
||||
matches: search_results.length,
|
||||
threshold: 90,
|
||||
results:
|
||||
search_results
|
||||
.take(5)
|
||||
.map do |result|
|
||||
post = result.fingerprint.post_file&.post
|
||||
{
|
||||
similarity: result.similarity_percentage.round(1),
|
||||
post_id: post&.id,
|
||||
url: post&.external_url_for_view,
|
||||
}
|
||||
end,
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
telegram_log: TelegramBotLog,
|
||||
response_text: String,
|
||||
processed_blob: T.nilable(BlobFile),
|
||||
).void
|
||||
end
|
||||
def update_telegram_log_no_results(
|
||||
telegram_log,
|
||||
response_text,
|
||||
processed_blob
|
||||
)
|
||||
telegram_log.update!(
|
||||
status: :no_results,
|
||||
search_results_count: 0,
|
||||
processed_image: processed_blob,
|
||||
response_data: {
|
||||
response_text: response_text,
|
||||
matches: 0,
|
||||
threshold: 90,
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
sig { params(telegram_log: TelegramBotLog, response_text: String).void }
|
||||
def update_telegram_log_invalid_image(telegram_log, response_text)
|
||||
telegram_log.update!(
|
||||
status: :invalid_image,
|
||||
search_results_count: 0,
|
||||
error_message: "Invalid or unsupported image format",
|
||||
response_data: {
|
||||
response_text: response_text,
|
||||
error: "Invalid image format",
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
sig { params(telegram_log: TelegramBotLog, error: StandardError).void }
|
||||
def update_telegram_log_error(telegram_log, error)
|
||||
telegram_log.update!(
|
||||
status: :error,
|
||||
search_results_count: 0,
|
||||
error_message: error.message,
|
||||
response_data: {
|
||||
error: error.message,
|
||||
error_class: error.class.name,
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
results: T::Array[Domain::VisualSearchHelper::SimilarFingerprintResult],
|
||||
|
||||
91
app/models/telegram_bot_log.rb
Normal file
91
app/models/telegram_bot_log.rb
Normal file
@@ -0,0 +1,91 @@
|
||||
# typed: strict
|
||||
class TelegramBotLog < ReduxApplicationRecord
|
||||
self.table_name = "telegram_bot_logs"
|
||||
|
||||
# Association to processed image blob
|
||||
belongs_to :processed_image,
|
||||
foreign_key: :processed_image_sha256,
|
||||
class_name: "::BlobFile",
|
||||
optional: true
|
||||
|
||||
# Status enum for tracking request outcomes
|
||||
enum :status,
|
||||
{
|
||||
success: "success",
|
||||
error: "error",
|
||||
no_results: "no_results",
|
||||
invalid_image: "invalid_image",
|
||||
},
|
||||
prefix: true
|
||||
|
||||
# Validations
|
||||
validates :telegram_user_id, presence: true
|
||||
validates :telegram_chat_id, presence: true
|
||||
validates :request_timestamp, presence: true
|
||||
validates :status, presence: true
|
||||
validates :search_results_count,
|
||||
presence: true,
|
||||
numericality: {
|
||||
greater_than_or_equal_to: 0,
|
||||
}
|
||||
validates :download_time, numericality: { greater_than: 0 }, allow_nil: true
|
||||
validates :image_processing_time,
|
||||
numericality: {
|
||||
greater_than: 0,
|
||||
},
|
||||
allow_nil: true
|
||||
validates :fingerprint_computation_time,
|
||||
numericality: {
|
||||
greater_than: 0,
|
||||
},
|
||||
allow_nil: true
|
||||
validates :search_computation_time,
|
||||
numericality: {
|
||||
greater_than: 0,
|
||||
},
|
||||
allow_nil: true
|
||||
validates :total_request_time,
|
||||
numericality: {
|
||||
greater_than: 0,
|
||||
},
|
||||
allow_nil: true
|
||||
validates :processed_image_sha256, length: { is: 32 }, allow_nil: true
|
||||
|
||||
# Scopes for common queries
|
||||
scope :for_user,
|
||||
->(telegram_user_id) { where(telegram_user_id: telegram_user_id) }
|
||||
scope :successful, -> { where(status: :success) }
|
||||
scope :with_results, -> { where("search_results_count > 0") }
|
||||
scope :recent, -> { order(request_timestamp: :desc) }
|
||||
scope :by_date_range,
|
||||
->(start_date, end_date) do
|
||||
where(request_timestamp: start_date..end_date)
|
||||
end
|
||||
|
||||
# Helper methods
|
||||
sig { returns(String) }
|
||||
def user_display_name
|
||||
if telegram_first_name.present? && telegram_last_name.present?
|
||||
"#{telegram_first_name} #{telegram_last_name}"
|
||||
elsif telegram_first_name.present?
|
||||
T.must(telegram_first_name)
|
||||
elsif telegram_username.present?
|
||||
"@#{telegram_username}"
|
||||
else
|
||||
"User #{telegram_user_id}"
|
||||
end
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def has_performance_metrics?
|
||||
download_time.present? && fingerprint_computation_time.present? &&
|
||||
search_computation_time.present? && total_request_time.present?
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def has_complete_performance_metrics?
|
||||
download_time.present? && image_processing_time.present? &&
|
||||
fingerprint_computation_time.present? &&
|
||||
search_computation_time.present? && total_request_time.present?
|
||||
end
|
||||
end
|
||||
27
app/policies/telegram_bot_log_policy.rb
Normal file
27
app/policies/telegram_bot_log_policy.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
# typed: strict
|
||||
class TelegramBotLogPolicy < ApplicationPolicy
|
||||
extend T::Sig
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def index?
|
||||
is_real_user? && is_role_admin?
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def show?
|
||||
is_real_user? && is_role_admin?
|
||||
end
|
||||
|
||||
class Scope < ApplicationPolicy::Scope
|
||||
extend T::Sig
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def resolve
|
||||
if @user&.admin?
|
||||
scope.all
|
||||
else
|
||||
scope.where(id: nil) # Returns empty relation for non-admin users
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -117,6 +117,10 @@
|
||||
<i class="fas fa-list mr-3 w-4 text-slate-400"></i>
|
||||
<span>Log Entries</span>
|
||||
<% end %>
|
||||
<%= link_to telegram_bot_logs_path, class: "flex w-full items-center px-4 py-2 text-sm text-slate-700 transition-colors hover:bg-slate-50 hover:text-slate-900" do %>
|
||||
<i class="fas fa-robot mr-3 w-4 text-slate-400"></i>
|
||||
<span>Telegram Bot Logs</span>
|
||||
<% end %>
|
||||
<div class="my-2 border-t border-slate-200"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
215
app/views/telegram_bot_logs/index.html.erb
Normal file
215
app/views/telegram_bot_logs/index.html.erb
Normal file
@@ -0,0 +1,215 @@
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-2xl font-semibold text-slate-900">Telegram Bot Audit Logs</h1>
|
||||
<p class="mt-2 text-sm text-slate-700">
|
||||
Comprehensive audit trail of all Telegram bot interactions and performance metrics.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
|
||||
<div class="text-sm text-slate-600">
|
||||
Showing <%= @telegram_bot_logs.count %> of <%= @telegram_bot_logs.total_count %> logs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Filter Form -->
|
||||
<div class="mt-6 bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-4 border-b border-slate-200">
|
||||
<h3 class="text-lg font-medium text-slate-900">Filters</h3>
|
||||
<%= form_with url: telegram_bot_logs_path, method: :get, local: true, class: "mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4" do |f| %>
|
||||
<!-- User ID Filter -->
|
||||
<div>
|
||||
<%= f.label :telegram_user_id, "Telegram User ID", class: "block text-sm font-medium text-slate-700" %>
|
||||
<%= f.number_field :telegram_user_id,
|
||||
value: @filter_params[:telegram_user_id],
|
||||
class: "mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
||||
placeholder: "Enter user ID" %>
|
||||
</div>
|
||||
<!-- Status Filter -->
|
||||
<div>
|
||||
<%= f.label :status, "Status", class: "block text-sm font-medium text-slate-700" %>
|
||||
<%= f.select :status,
|
||||
options_for_select([["All Statuses", ""]] + @status_options.map { |s| [s.humanize, s] }, @filter_params[:status]),
|
||||
{},
|
||||
{ class: "mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" } %>
|
||||
</div>
|
||||
<!-- Date Range Filters -->
|
||||
<div>
|
||||
<%= f.label :start_date, "Start Date", class: "block text-sm font-medium text-slate-700" %>
|
||||
<%= f.date_field :start_date,
|
||||
value: @filter_params[:start_date],
|
||||
class: "mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
</div>
|
||||
<div>
|
||||
<%= f.label :end_date, "End Date", class: "block text-sm font-medium text-slate-700" %>
|
||||
<%= f.date_field :end_date,
|
||||
value: @filter_params[:end_date],
|
||||
class: "mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
</div>
|
||||
<!-- Results Count Filters -->
|
||||
<div>
|
||||
<%= f.label :min_results, "Min Results", class: "block text-sm font-medium text-slate-700" %>
|
||||
<%= f.number_field :min_results,
|
||||
value: @filter_params[:min_results],
|
||||
min: 0,
|
||||
class: "mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
||||
placeholder: "Min results" %>
|
||||
</div>
|
||||
<div>
|
||||
<%= f.label :max_results, "Max Results", class: "block text-sm font-medium text-slate-700" %>
|
||||
<%= f.number_field :max_results,
|
||||
value: @filter_params[:max_results],
|
||||
min: 0,
|
||||
class: "mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
||||
placeholder: "Max results" %>
|
||||
</div>
|
||||
<!-- Performance Filter -->
|
||||
<div class="flex items-center">
|
||||
<%= f.check_box :slow_requests,
|
||||
{ checked: @filter_params[:slow_requests] == "true", class: "h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500" },
|
||||
"true", "" %>
|
||||
<%= f.label :slow_requests, "Show slow requests only", class: "ml-2 block text-sm text-slate-700" %>
|
||||
</div>
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-end gap-2">
|
||||
<%= f.submit "Apply Filters", class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded text-sm" %>
|
||||
<%= link_to "Clear", telegram_bot_logs_path, class: "bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded text-sm" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Logs Table -->
|
||||
<% if @telegram_bot_logs.any? %>
|
||||
<div class="mt-6 overflow-hidden bg-white shadow sm:rounded-lg">
|
||||
<div class="p-3 sm:p-4">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-300">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="pb-2 text-left text-sm font-semibold text-slate-900">User</th>
|
||||
<th class="pb-2 text-left text-sm font-semibold text-slate-900">Status</th>
|
||||
<th class="pb-2 text-left text-sm font-semibold text-slate-900">Results</th>
|
||||
<th class="pb-2 text-left text-sm font-semibold text-slate-900">Performance</th>
|
||||
<th class="pb-2 text-left text-sm font-semibold text-slate-900">Timestamp</th>
|
||||
<th class="relative pb-2"><span class="sr-only">Actions</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-200">
|
||||
<% @telegram_bot_logs.each do |log| %>
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="py-3 pr-4 text-sm">
|
||||
<div class="font-medium text-slate-900 truncate max-w-32">
|
||||
<%= log.user_display_name %>
|
||||
</div>
|
||||
<div class="text-slate-500 text-xs">
|
||||
ID: <%= log.telegram_user_id %>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-3 pr-4 text-sm">
|
||||
<% status_color = case log.status
|
||||
when "success"
|
||||
"bg-green-100 text-green-800"
|
||||
when "error"
|
||||
"bg-red-100 text-red-800"
|
||||
when "no_results"
|
||||
"bg-yellow-100 text-yellow-800"
|
||||
when "invalid_image"
|
||||
"bg-orange-100 text-orange-800"
|
||||
else
|
||||
"bg-slate-100 text-slate-800"
|
||||
end %>
|
||||
<span class="<%= status_color %> inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium">
|
||||
<%= log.status.humanize %>
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 pr-4 text-sm text-slate-500">
|
||||
<div class="font-medium text-slate-900">
|
||||
<%= log.search_results_count %>
|
||||
</div>
|
||||
<% if log.search_results_count > 0 %>
|
||||
<div class="text-xs text-green-600">
|
||||
matches found
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-xs text-slate-400">
|
||||
no matches
|
||||
</div>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="py-3 pr-4 text-sm text-slate-500">
|
||||
<% if log.has_performance_metrics? %>
|
||||
<div class="text-xs space-y-1">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-blue-600">Download:</span>
|
||||
<span class="font-mono"><%= sprintf("%.3f", log.download_time) %>s</span>
|
||||
</div>
|
||||
<% if log.image_processing_time.present? %>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-purple-600">Process:</span>
|
||||
<span class="font-mono"><%= sprintf("%.3f", log.image_processing_time) %>s</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-yellow-600">Fingerprint:</span>
|
||||
<span class="font-mono"><%= sprintf("%.3f", log.fingerprint_computation_time) %>s</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-green-600">Search:</span>
|
||||
<span class="font-mono"><%= sprintf("%.3f", log.search_computation_time) %>s</span>
|
||||
</div>
|
||||
<div class="flex justify-between font-medium border-t pt-1">
|
||||
<span>Total:</span>
|
||||
<span class="font-mono
|
||||
<%= log.total_request_time && log.total_request_time > 1.0 ? 'text-red-600' : 'text-slate-700' %>">
|
||||
<%= log.total_request_time ? sprintf("%.3f", log.total_request_time) : 'N/A' %>s
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<span class="text-slate-400 text-xs">No metrics</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="py-3 pr-4 text-sm text-slate-500">
|
||||
<div class="text-xs">
|
||||
<%= log.request_timestamp.strftime("%m/%d/%Y") %>
|
||||
</div>
|
||||
<div class="text-xs font-mono">
|
||||
<%= log.request_timestamp.strftime("%H:%M:%S") %>
|
||||
</div>
|
||||
<div class="text-xs text-slate-400">
|
||||
<%= time_ago_in_words(log.request_timestamp) %> ago
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-3 text-right text-sm font-medium">
|
||||
<%= link_to "View", telegram_bot_log_path(log),
|
||||
class: "text-blue-600 hover:text-blue-800 font-medium" %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<% if @telegram_bot_logs.respond_to?(:current_page) %>
|
||||
<%= render "shared/pagination_controls", collection: @telegram_bot_logs %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="mt-6 bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-6 text-center">
|
||||
<div class="mx-auto h-12 w-12 text-slate-400">
|
||||
<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-12 h-12">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3-3v-6a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6a2.25 2.25 0 002.25 2.25h10.5A2.25 2.25 0 0019.5 15z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mt-2 text-sm font-semibold text-slate-900">No logs found</h3>
|
||||
<p class="mt-1 text-sm text-slate-500">No Telegram bot logs match your current filters.</p>
|
||||
<div class="mt-6">
|
||||
<%= link_to "Clear filters", telegram_bot_logs_path,
|
||||
class: "inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
276
app/views/telegram_bot_logs/show.html.erb
Normal file
276
app/views/telegram_bot_logs/show.html.erb
Normal file
@@ -0,0 +1,276 @@
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-2xl font-semibold text-slate-900">Telegram Bot Log Details</h1>
|
||||
<p class="mt-2 text-sm text-slate-700">
|
||||
Detailed view of Telegram bot interaction #<%= @telegram_bot_log.id %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
|
||||
<%= link_to "← Back to Logs", telegram_bot_logs_path,
|
||||
class: "bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<!-- User Information -->
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-slate-900">User Information</h3>
|
||||
<div class="mt-4 space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-slate-500">Display Name</dt>
|
||||
<dd class="text-sm text-slate-900 font-medium"><%= @telegram_bot_log.user_display_name %></dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-slate-500">Telegram User ID</dt>
|
||||
<dd class="text-sm text-slate-900 font-mono"><%= @telegram_bot_log.telegram_user_id %></dd>
|
||||
</div>
|
||||
<% if @telegram_bot_log.telegram_username.present? %>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-slate-500">Username</dt>
|
||||
<dd class="text-sm text-slate-900">@<%= @telegram_bot_log.telegram_username %></dd>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @telegram_bot_log.telegram_first_name.present? %>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-slate-500">First Name</dt>
|
||||
<dd class="text-sm text-slate-900"><%= @telegram_bot_log.telegram_first_name %></dd>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @telegram_bot_log.telegram_last_name.present? %>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-slate-500">Last Name</dt>
|
||||
<dd class="text-sm text-slate-900"><%= @telegram_bot_log.telegram_last_name %></dd>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-slate-500">Chat ID</dt>
|
||||
<dd class="text-sm text-slate-900 font-mono"><%= @telegram_bot_log.telegram_chat_id %></dd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Request Information -->
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-slate-900">Request Information</h3>
|
||||
<div class="mt-4 space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-slate-500">Status</dt>
|
||||
<dd class="text-sm">
|
||||
<% status_color = case @telegram_bot_log.status
|
||||
when "success"
|
||||
"bg-green-100 text-green-800"
|
||||
when "error"
|
||||
"bg-red-100 text-red-800"
|
||||
when "no_results"
|
||||
"bg-yellow-100 text-yellow-800"
|
||||
when "invalid_image"
|
||||
"bg-orange-100 text-orange-800"
|
||||
else
|
||||
"bg-slate-100 text-slate-800"
|
||||
end %>
|
||||
<span class="<%= status_color %> inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium">
|
||||
<%= @telegram_bot_log.status.humanize %>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-slate-500">Timestamp</dt>
|
||||
<dd class="text-sm text-slate-900">
|
||||
<div><%= @telegram_bot_log.request_timestamp.strftime("%B %d, %Y at %I:%M:%S %p") %></div>
|
||||
<div class="text-xs text-slate-500"><%= time_ago_in_words(@telegram_bot_log.request_timestamp) %> ago</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-slate-500">Search Results</dt>
|
||||
<dd class="text-sm text-slate-900">
|
||||
<span class="font-medium"><%= @telegram_bot_log.search_results_count %></span>
|
||||
<% if @telegram_bot_log.search_results_count > 0 %>
|
||||
<span class="text-green-600 text-xs ml-1">matches found</span>
|
||||
<% else %>
|
||||
<span class="text-slate-400 text-xs ml-1">no matches</span>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
<% if @telegram_bot_log.error_message.present? %>
|
||||
<div class="space-y-1">
|
||||
<dt class="text-sm font-medium text-slate-500">Error Message</dt>
|
||||
<dd class="text-sm text-red-700 bg-red-50 p-2 rounded border">
|
||||
<%= @telegram_bot_log.error_message %>
|
||||
</dd>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Performance Metrics -->
|
||||
<% if @telegram_bot_log.has_performance_metrics? %>
|
||||
<div class="mt-6 bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-slate-900">Performance Metrics</h3>
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5">
|
||||
<div class="bg-blue-50 p-4 rounded-lg">
|
||||
<div class="text-sm font-medium text-slate-500">Download Time</div>
|
||||
<div class="text-xl font-bold text-blue-700 font-mono">
|
||||
<%= sprintf("%.3f", @telegram_bot_log.download_time) %>s
|
||||
</div>
|
||||
<div class="text-xs text-blue-600">From Telegram</div>
|
||||
</div>
|
||||
<% if @telegram_bot_log.image_processing_time.present? %>
|
||||
<div class="bg-purple-50 p-4 rounded-lg">
|
||||
<div class="text-sm font-medium text-slate-500">Processing Time</div>
|
||||
<div class="text-xl font-bold text-purple-700 font-mono">
|
||||
<%= sprintf("%.3f", @telegram_bot_log.image_processing_time) %>s
|
||||
</div>
|
||||
<div class="text-xs text-purple-600">File handling</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="bg-yellow-50 p-4 rounded-lg">
|
||||
<div class="text-sm font-medium text-slate-500">Fingerprint Time</div>
|
||||
<div class="text-xl font-bold text-yellow-700 font-mono">
|
||||
<%= sprintf("%.3f", @telegram_bot_log.fingerprint_computation_time) %>s
|
||||
</div>
|
||||
<div class="text-xs text-yellow-600">Image analysis</div>
|
||||
</div>
|
||||
<div class="bg-green-50 p-4 rounded-lg">
|
||||
<div class="text-sm font-medium text-slate-500">Search Time</div>
|
||||
<div class="text-xl font-bold text-green-700 font-mono">
|
||||
<%= sprintf("%.3f", @telegram_bot_log.search_computation_time) %>s
|
||||
</div>
|
||||
<div class="text-xs text-green-600">Find matches</div>
|
||||
</div>
|
||||
<div class="bg-slate-50 p-4 rounded-lg">
|
||||
<div class="text-sm font-medium text-slate-500">Total Request Time</div>
|
||||
<div class="text-xl font-bold font-mono
|
||||
<%= @telegram_bot_log.total_request_time && @telegram_bot_log.total_request_time > 1.0 ? 'text-red-600' : 'text-slate-700' %>">
|
||||
<%= @telegram_bot_log.total_request_time ? sprintf("%.3f", @telegram_bot_log.total_request_time) : 'N/A' %>s
|
||||
</div>
|
||||
<div class="text-xs
|
||||
<%= @telegram_bot_log.total_request_time && @telegram_bot_log.total_request_time > 1.0 ? 'text-red-500' : 'text-slate-500' %>">
|
||||
<%= @telegram_bot_log.total_request_time && @telegram_bot_log.total_request_time > 1.0 ? 'Slow request' : 'Normal speed' %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<!-- Processed Image -->
|
||||
<% if @telegram_bot_log.processed_image.present? %>
|
||||
<div class="mt-6 bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-slate-900">Processed Image</h3>
|
||||
<div class="mt-4 space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-slate-500">Content Type</dt>
|
||||
<dd class="text-sm text-slate-900 font-mono"><%= @telegram_bot_log.processed_image.content_type %></dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-slate-500">File Size</dt>
|
||||
<dd class="text-sm text-slate-900"><%= number_to_human_size(@telegram_bot_log.processed_image.size_bytes) %></dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-slate-500">SHA256 Hash</dt>
|
||||
<dd class="text-sm text-slate-900 font-mono break-all"><%= @telegram_bot_log.processed_image.sha256.unpack1('H*') %></dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-slate-500">Created</dt>
|
||||
<dd class="text-sm text-slate-900">
|
||||
<%= @telegram_bot_log.processed_image.created_at.strftime("%B %d, %Y at %I:%M:%S %p") %>
|
||||
<div class="text-xs text-slate-500"><%= time_ago_in_words(@telegram_bot_log.processed_image.created_at) %> ago</div>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<!-- Response Data -->
|
||||
<% if @telegram_bot_log.response_data.present? && @telegram_bot_log.response_data.any? %>
|
||||
<div class="mt-6 bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-slate-900">Response Data</h3>
|
||||
<div class="mt-4">
|
||||
<% if @telegram_bot_log.status == "success" && @telegram_bot_log.response_data["results"].present? %>
|
||||
<!-- Success Response with Results -->
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium text-slate-500">Matches Found:</span>
|
||||
<span class="text-slate-900"><%= @telegram_bot_log.response_data["matches"] %></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium text-slate-500">Similarity Threshold:</span>
|
||||
<span class="text-slate-900"><%= @telegram_bot_log.response_data["threshold"] %>%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-slate-200 pt-4">
|
||||
<h4 class="font-medium text-slate-900 mb-3">Search Results</h4>
|
||||
<div class="space-y-2">
|
||||
<% @telegram_bot_log.response_data["results"].each_with_index do |result, index| %>
|
||||
<div class="bg-slate-50 p-3 rounded border">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-slate-900">
|
||||
Result #<%= index + 1 %>
|
||||
</div>
|
||||
<% if result["post_id"] %>
|
||||
<div class="text-xs text-slate-500">Post ID: <%= result["post_id"] %></div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-sm font-bold
|
||||
<%= result["similarity"] >= 95 ? 'text-green-600' : result["similarity"] >= 90 ? 'text-yellow-600' : 'text-red-600' %>">
|
||||
<%= result["similarity"] %>% similar
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% if result["url"] %>
|
||||
<div class="mt-2">
|
||||
<%= link_to result["url"], result["url"],
|
||||
target: "_blank",
|
||||
class: "text-blue-600 hover:text-blue-800 text-xs break-all" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<!-- Raw JSON Response -->
|
||||
<div class="bg-slate-50 p-4 rounded-lg border">
|
||||
<pre class="text-sm text-slate-900 whitespace-pre-wrap break-words"><%= JSON.pretty_generate(@telegram_bot_log.response_data) %></pre>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<!-- System Information -->
|
||||
<div class="mt-6 bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-slate-900">System Information</h3>
|
||||
<div class="mt-4 space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-slate-500">Record ID</dt>
|
||||
<dd class="text-sm text-slate-900 font-mono"><%= @telegram_bot_log.id %></dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-slate-500">Created At</dt>
|
||||
<dd class="text-sm text-slate-900">
|
||||
<%= @telegram_bot_log.created_at.strftime("%B %d, %Y at %I:%M:%S %p") %>
|
||||
<div class="text-xs text-slate-500"><%= time_ago_in_words(@telegram_bot_log.created_at) %> ago</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-slate-500">Last Updated</dt>
|
||||
<dd class="text-sm text-slate-900">
|
||||
<%= @telegram_bot_log.updated_at.strftime("%B %d, %Y at %I:%M:%S %p") %>
|
||||
<div class="text-xs text-slate-500"><%= time_ago_in_words(@telegram_bot_log.updated_at) %> ago</div>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,6 +121,11 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
# Telegram bot audit logs
|
||||
resources :telegram_bot_logs,
|
||||
only: %i[index show],
|
||||
path: "telegram-bot-logs"
|
||||
|
||||
mount GoodJob::Engine => "jobs"
|
||||
mount PgHero::Engine => "pghero"
|
||||
|
||||
|
||||
42
db/migrate/20250731035548_create_telegram_bot_logs.rb
Normal file
42
db/migrate/20250731035548_create_telegram_bot_logs.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
# typed: true
|
||||
class CreateTelegramBotLogs < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
create_table :telegram_bot_logs do |t|
|
||||
# Telegram user information
|
||||
t.bigint :telegram_user_id, null: false
|
||||
t.string :telegram_username
|
||||
t.string :telegram_first_name
|
||||
t.string :telegram_last_name
|
||||
t.bigint :telegram_chat_id, null: false
|
||||
|
||||
# Request metadata
|
||||
t.timestamp :request_timestamp, null: false
|
||||
t.string :status, null: false
|
||||
t.text :error_message
|
||||
|
||||
# Performance metrics
|
||||
t.float :fingerprint_computation_time
|
||||
t.float :search_computation_time
|
||||
t.integer :search_results_count, default: 0
|
||||
|
||||
# Response and image data
|
||||
t.jsonb :response_data, default: {}
|
||||
t.binary :processed_image_sha256
|
||||
|
||||
t.timestamps
|
||||
|
||||
# Indexes for performance
|
||||
t.index :telegram_user_id
|
||||
t.index :request_timestamp
|
||||
t.index :search_results_count
|
||||
t.index :processed_image_sha256
|
||||
t.index %i[telegram_user_id request_timestamp]
|
||||
end
|
||||
|
||||
# Foreign key constraint to BlobFile (sha256 field)
|
||||
add_foreign_key :telegram_bot_logs,
|
||||
:blob_files,
|
||||
column: :processed_image_sha256,
|
||||
primary_key: :sha256
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class AddPerformanceTimingToTelegramBotLogs < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_column :telegram_bot_logs, :download_time, :float
|
||||
add_column :telegram_bot_logs, :image_processing_time, :float
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddTotalRequestTimeToTelegramBotLogs < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_column :telegram_bot_logs, :total_request_time, :float
|
||||
end
|
||||
end
|
||||
107
db/structure.sql
107
db/structure.sql
@@ -2324,6 +2324,52 @@ CREATE TABLE public.schema_migrations (
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: telegram_bot_logs; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public.telegram_bot_logs (
|
||||
id bigint NOT NULL,
|
||||
telegram_user_id bigint NOT NULL,
|
||||
telegram_username character varying,
|
||||
telegram_first_name character varying,
|
||||
telegram_last_name character varying,
|
||||
telegram_chat_id bigint NOT NULL,
|
||||
request_timestamp timestamp without time zone NOT NULL,
|
||||
status character varying NOT NULL,
|
||||
error_message text,
|
||||
fingerprint_computation_time double precision,
|
||||
search_computation_time double precision,
|
||||
search_results_count integer DEFAULT 0,
|
||||
response_data jsonb DEFAULT '{}'::jsonb,
|
||||
processed_image_sha256 bytea,
|
||||
created_at timestamp(6) without time zone NOT NULL,
|
||||
updated_at timestamp(6) without time zone NOT NULL,
|
||||
download_time double precision,
|
||||
image_processing_time double precision,
|
||||
total_request_time double precision
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: telegram_bot_logs_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.telegram_bot_logs_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
--
|
||||
-- Name: telegram_bot_logs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.telegram_bot_logs_id_seq OWNED BY public.telegram_bot_logs.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: trained_regression_models; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
@@ -3050,6 +3096,13 @@ ALTER TABLE ONLY public.pghero_query_stats ALTER COLUMN id SET DEFAULT nextval('
|
||||
ALTER TABLE ONLY public.pghero_space_stats ALTER COLUMN id SET DEFAULT nextval('public.pghero_space_stats_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: telegram_bot_logs id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.telegram_bot_logs ALTER COLUMN id SET DEFAULT nextval('public.telegram_bot_logs_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: trained_regression_models id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
@@ -3336,6 +3389,14 @@ ALTER TABLE ONLY public.schema_migrations
|
||||
ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version);
|
||||
|
||||
|
||||
--
|
||||
-- Name: telegram_bot_logs telegram_bot_logs_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.telegram_bot_logs
|
||||
ADD CONSTRAINT telegram_bot_logs_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: trained_regression_models trained_regression_models_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@@ -3541,6 +3602,13 @@ CREATE UNIQUE INDEX idx_on_post_file_id_thumb_type_frame_17152086d1 ON public.do
|
||||
CREATE UNIQUE INDEX idx_on_post_file_id_thumbnail_id_28e9a641fb ON public.domain_post_file_bit_fingerprints USING btree (post_file_id, thumbnail_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idx_on_telegram_user_id_request_timestamp_075ae9ab44; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX idx_on_telegram_user_id_request_timestamp_075ae9ab44 ON public.telegram_bot_logs USING btree (telegram_user_id, request_timestamp);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_blob_files_on_sha256; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -4619,6 +4687,34 @@ CREATE INDEX index_pghero_query_stats_on_database_and_captured_at ON public.pghe
|
||||
CREATE INDEX index_pghero_space_stats_on_database_and_captured_at ON public.pghero_space_stats USING btree (database, captured_at);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_telegram_bot_logs_on_processed_image_sha256; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_telegram_bot_logs_on_processed_image_sha256 ON public.telegram_bot_logs USING btree (processed_image_sha256);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_telegram_bot_logs_on_request_timestamp; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_telegram_bot_logs_on_request_timestamp ON public.telegram_bot_logs USING btree (request_timestamp);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_telegram_bot_logs_on_search_results_count; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_telegram_bot_logs_on_search_results_count ON public.telegram_bot_logs USING btree (search_results_count);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_telegram_bot_logs_on_telegram_user_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_telegram_bot_logs_on_telegram_user_id ON public.telegram_bot_logs USING btree (telegram_user_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_trained_regression_models_on_created_at; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -5130,6 +5226,14 @@ ALTER INDEX public.index_blob_files_on_sha256 ATTACH PARTITION public.index_blob
|
||||
ALTER INDEX public.index_blob_files_on_sha256 ATTACH PARTITION public.index_blob_files_63_on_sha256;
|
||||
|
||||
|
||||
--
|
||||
-- Name: telegram_bot_logs fk_rails_001ca2ed89; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.telegram_bot_logs
|
||||
ADD CONSTRAINT fk_rails_001ca2ed89 FOREIGN KEY (processed_image_sha256) REFERENCES public.blob_files(sha256);
|
||||
|
||||
|
||||
--
|
||||
-- Name: domain_user_job_event_add_tracked_objects fk_rails_03ea351597; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@@ -5449,6 +5553,9 @@ ALTER TABLE ONLY public.domain_twitter_tweets
|
||||
SET search_path TO "$user", public;
|
||||
|
||||
INSERT INTO "schema_migrations" (version) VALUES
|
||||
('20250805045947'),
|
||||
('20250805044757'),
|
||||
('20250731035548'),
|
||||
('20250727173100'),
|
||||
('20250726051748'),
|
||||
('20250726051451'),
|
||||
|
||||
18
sorbet/rbi/dsl/active_support/test_case.rbi
generated
Normal file
18
sorbet/rbi/dsl/active_support/test_case.rbi
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
# typed: true
|
||||
|
||||
# DO NOT EDIT MANUALLY
|
||||
# This is an autogenerated file for dynamic methods in `ActiveSupport::TestCase`.
|
||||
# Please instead update this file by running `bin/tapioca dsl ActiveSupport::TestCase`.
|
||||
|
||||
|
||||
class ActiveSupport::TestCase
|
||||
sig { params(fixture_name: NilClass, other_fixtures: NilClass).returns(T::Array[TelegramBotLog]) }
|
||||
sig { params(fixture_name: T.any(String, Symbol), other_fixtures: NilClass).returns(TelegramBotLog) }
|
||||
sig do
|
||||
params(
|
||||
fixture_name: T.any(String, Symbol),
|
||||
other_fixtures: T.any(String, Symbol)
|
||||
).returns(T::Array[TelegramBotLog])
|
||||
end
|
||||
def telegram_bot_logs(fixture_name = nil, *other_fixtures); end
|
||||
end
|
||||
12
sorbet/rbi/dsl/generated_path_helpers_module.rbi
generated
12
sorbet/rbi/dsl/generated_path_helpers_module.rbi
generated
@@ -216,6 +216,18 @@ module GeneratedPathHelpersModule
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def stats_log_entries_path(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def telegram_bot_log_path(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def telegram_bot_logs_path(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def telegram_config_edit_global_states_path(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def telegram_config_global_states_path(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def toggle_state_ip_address_role_path(*args); end
|
||||
|
||||
|
||||
12
sorbet/rbi/dsl/generated_url_helpers_module.rbi
generated
12
sorbet/rbi/dsl/generated_url_helpers_module.rbi
generated
@@ -216,6 +216,18 @@ module GeneratedUrlHelpersModule
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def stats_log_entries_url(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def telegram_bot_log_url(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def telegram_bot_logs_url(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def telegram_config_edit_global_states_url(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def telegram_config_global_states_url(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def toggle_state_ip_address_role_url(*args); end
|
||||
|
||||
|
||||
2249
sorbet/rbi/dsl/telegram_bot_log.rbi
generated
Normal file
2249
sorbet/rbi/dsl/telegram_bot_log.rbi
generated
Normal file
File diff suppressed because it is too large
Load Diff
267
spec/controllers/telegram_bot_logs_controller_spec.rb
Normal file
267
spec/controllers/telegram_bot_logs_controller_spec.rb
Normal file
@@ -0,0 +1,267 @@
|
||||
# typed: false
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe TelegramBotLogsController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:admin_user) { create(:user, role: :admin) }
|
||||
let(:regular_user) { create(:user, role: :user) }
|
||||
let(:telegram_bot_log) { create(:telegram_bot_log, :successful, :with_image) }
|
||||
|
||||
describe "authorization" do
|
||||
context "when user is not signed in" do
|
||||
describe "GET #index" do
|
||||
it "redirects to sign in" do
|
||||
get :index
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET #show" do
|
||||
it "redirects to sign in" do
|
||||
get :show, params: { id: telegram_bot_log.id }
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when regular user is signed in" do
|
||||
before { sign_in regular_user }
|
||||
|
||||
describe "GET #index" do
|
||||
it "redirects to root with access denied" do
|
||||
get :index
|
||||
expect(response).to redirect_to(root_path)
|
||||
expect(flash[:alert]).to eq(
|
||||
"You are not authorized to perform this action.",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET #show" do
|
||||
it "redirects to root with access denied" do
|
||||
get :show, params: { id: telegram_bot_log.id }
|
||||
expect(response).to redirect_to(root_path)
|
||||
expect(flash[:alert]).to eq(
|
||||
"You are not authorized to perform this action.",
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when admin user is signed in" do
|
||||
before { sign_in admin_user }
|
||||
|
||||
describe "GET #index" do
|
||||
it "returns a success response" do
|
||||
get :index
|
||||
expect(response).to be_successful
|
||||
expect(response).to render_template(:index)
|
||||
end
|
||||
|
||||
it "assigns telegram_bot_logs with proper pagination" do
|
||||
logs = create_list(:telegram_bot_log, 3, :successful)
|
||||
get :index
|
||||
expect(assigns(:telegram_bot_logs)).to be_present
|
||||
expect(assigns(:telegram_bot_logs)).to respond_to(:current_page)
|
||||
expect(assigns(:telegram_bot_logs).to_a).to match_array(logs)
|
||||
end
|
||||
|
||||
it "respects limit parameter" do
|
||||
create_list(:telegram_bot_log, 5, :successful)
|
||||
get :index, params: { limit: 2 }
|
||||
expect(assigns(:limit)).to eq(2)
|
||||
expect(assigns(:telegram_bot_logs).count).to eq(2)
|
||||
end
|
||||
|
||||
it "clamps limit parameter within bounds" do
|
||||
get :index, params: { limit: 1000 }
|
||||
expect(assigns(:limit)).to eq(500) # max limit
|
||||
|
||||
get :index, params: { limit: 0 }
|
||||
expect(assigns(:limit)).to eq(1) # min limit
|
||||
end
|
||||
|
||||
it "handles pagination parameters" do
|
||||
create_list(:telegram_bot_log, 10, :successful)
|
||||
get :index, params: { page: 2, limit: 5 }
|
||||
expect(assigns(:telegram_bot_logs).current_page).to eq(2)
|
||||
end
|
||||
|
||||
it "assigns status options for filtering" do
|
||||
get :index
|
||||
expect(assigns(:status_options)).to eq(TelegramBotLog.statuses.keys)
|
||||
end
|
||||
|
||||
it "assigns filter params" do
|
||||
filter_params = {
|
||||
telegram_user_id: "123456789",
|
||||
status: "successful",
|
||||
start_date: "2023-01-01",
|
||||
end_date: "2023-12-31",
|
||||
min_results: "1",
|
||||
max_results: "10",
|
||||
slow_requests: "true",
|
||||
}
|
||||
|
||||
get :index, params: filter_params
|
||||
assigned_params = assigns(:filter_params)
|
||||
expect(assigned_params["telegram_user_id"]).to eq("123456789")
|
||||
expect(assigned_params["status"]).to eq("successful")
|
||||
expect(assigned_params["start_date"]).to eq("2023-01-01")
|
||||
expect(assigned_params["end_date"]).to eq("2023-12-31")
|
||||
expect(assigned_params["min_results"]).to eq("1")
|
||||
expect(assigned_params["max_results"]).to eq("10")
|
||||
expect(assigned_params["slow_requests"]).to eq("true")
|
||||
end
|
||||
|
||||
describe "filtering" do
|
||||
let!(:user_123_log) do
|
||||
create(:telegram_bot_log, :successful, telegram_user_id: 123_456_789)
|
||||
end
|
||||
let!(:user_456_log) do
|
||||
create(:telegram_bot_log, :successful, telegram_user_id: 456_789_123)
|
||||
end
|
||||
let!(:error_log) { create(:telegram_bot_log, :with_error) }
|
||||
let!(:no_results_log) { create(:telegram_bot_log, :no_results) }
|
||||
|
||||
it "filters by telegram_user_id" do
|
||||
get :index, params: { telegram_user_id: "123456789" }
|
||||
logs = assigns(:telegram_bot_logs).to_a
|
||||
expect(logs).to include(user_123_log)
|
||||
expect(logs).not_to include(user_456_log)
|
||||
end
|
||||
|
||||
it "filters by status" do
|
||||
get :index, params: { status: "error" }
|
||||
logs = assigns(:telegram_bot_logs).to_a
|
||||
expect(logs).to include(error_log)
|
||||
expect(logs).not_to include(user_123_log)
|
||||
end
|
||||
|
||||
it "filters by search results count range" do
|
||||
high_results_log =
|
||||
create(:telegram_bot_log, :successful, search_results_count: 5)
|
||||
low_results_log =
|
||||
create(:telegram_bot_log, :successful, search_results_count: 1)
|
||||
|
||||
get :index, params: { min_results: "3" }
|
||||
logs = assigns(:telegram_bot_logs).to_a
|
||||
expect(logs).to include(high_results_log)
|
||||
expect(logs).not_to include(low_results_log)
|
||||
end
|
||||
|
||||
it "filters by date range" do
|
||||
old_log =
|
||||
create(
|
||||
:telegram_bot_log,
|
||||
:successful,
|
||||
request_timestamp: 1.week.ago,
|
||||
)
|
||||
new_log =
|
||||
create(:telegram_bot_log, :successful, request_timestamp: 1.day.ago)
|
||||
|
||||
get :index,
|
||||
params: {
|
||||
start_date: 3.days.ago.to_date.to_s,
|
||||
end_date: Date.current.to_s,
|
||||
}
|
||||
logs = assigns(:telegram_bot_logs).to_a
|
||||
expect(logs).to include(new_log)
|
||||
expect(logs).not_to include(old_log)
|
||||
end
|
||||
|
||||
it "filters slow requests" do
|
||||
fast_log =
|
||||
create(
|
||||
:telegram_bot_log,
|
||||
:successful,
|
||||
fingerprint_computation_time: 0.1,
|
||||
search_computation_time: 0.2,
|
||||
)
|
||||
slow_log =
|
||||
create(
|
||||
:telegram_bot_log,
|
||||
:successful,
|
||||
fingerprint_computation_time: 0.8,
|
||||
search_computation_time: 0.9,
|
||||
)
|
||||
|
||||
get :index, params: { slow_requests: "true" }
|
||||
logs = assigns(:telegram_bot_logs).to_a
|
||||
expect(logs).to include(slow_log)
|
||||
expect(logs).not_to include(fast_log)
|
||||
end
|
||||
end
|
||||
|
||||
describe "includes associations" do
|
||||
it "includes processed_image association" do
|
||||
create(:telegram_bot_log, :with_image)
|
||||
get :index
|
||||
|
||||
# Verify the association is loaded
|
||||
expect(
|
||||
assigns(:telegram_bot_logs).first&.processed_image,
|
||||
).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe "error handling" do
|
||||
it "handles invalid filter parameters gracefully" do
|
||||
get :index,
|
||||
params: {
|
||||
start_date: "invalid-date",
|
||||
min_results: "not-a-number",
|
||||
}
|
||||
expect(response).to be_successful
|
||||
expect(assigns(:telegram_bot_logs)).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET #show" do
|
||||
let(:telegram_bot_log) do
|
||||
create(:telegram_bot_log, :successful, :with_image)
|
||||
end
|
||||
|
||||
it "returns a success response" do
|
||||
get :show, params: { id: telegram_bot_log.id }
|
||||
expect(response).to be_successful
|
||||
expect(response).to render_template(:show)
|
||||
end
|
||||
|
||||
it "assigns the requested telegram_bot_log" do
|
||||
get :show, params: { id: telegram_bot_log.id }
|
||||
expect(assigns(:telegram_bot_log)).to eq(telegram_bot_log)
|
||||
end
|
||||
|
||||
it "includes processed_image association" do
|
||||
get :show, params: { id: telegram_bot_log.id }
|
||||
|
||||
# Verify the association is loaded
|
||||
expect(assigns(:telegram_bot_log).processed_image).to be_present
|
||||
end
|
||||
|
||||
it "handles non-existent log gracefully" do
|
||||
get :show, params: { id: 999_999 }
|
||||
expect(response).to redirect_to(telegram_bot_logs_path)
|
||||
expect(flash[:alert]).to eq("Telegram bot log not found.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "performance" do
|
||||
context "with many records" do
|
||||
before { create_list(:telegram_bot_log, 100, :successful) }
|
||||
|
||||
it "loads records successfully with many logs" do
|
||||
sign_in admin_user
|
||||
|
||||
get :index, params: { limit: 50 }
|
||||
expect(response).to be_successful
|
||||
expect(assigns(:telegram_bot_logs).count).to eq(50)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
97
spec/factories/telegram_bot_logs.rb
Normal file
97
spec/factories/telegram_bot_logs.rb
Normal file
@@ -0,0 +1,97 @@
|
||||
# typed: false
|
||||
FactoryBot.define do
|
||||
factory :telegram_bot_log do
|
||||
sequence(:telegram_user_id) { |n| 100_000_000 + n }
|
||||
sequence(:telegram_chat_id) { |n| 200_000_000 + n }
|
||||
telegram_username { "user#{SecureRandom.alphanumeric(6)}" }
|
||||
telegram_first_name { "Test" }
|
||||
telegram_last_name { "User" }
|
||||
request_timestamp { Time.current }
|
||||
status { :success }
|
||||
search_results_count { 3 }
|
||||
download_time { 0.08 }
|
||||
image_processing_time { 0.05 }
|
||||
fingerprint_computation_time { 0.15 }
|
||||
search_computation_time { 0.25 }
|
||||
total_request_time { 0.53 }
|
||||
response_data { { matches: 3, threshold: 90 } }
|
||||
|
||||
trait :with_image do
|
||||
association :processed_image, factory: %i[blob_file image_blob]
|
||||
end
|
||||
|
||||
trait :successful do
|
||||
status { :success }
|
||||
search_results_count { 5 }
|
||||
download_time { 0.06 }
|
||||
image_processing_time { 0.04 }
|
||||
fingerprint_computation_time { 0.12 }
|
||||
search_computation_time { 0.18 }
|
||||
total_request_time { 0.40 }
|
||||
response_data { { matches: 5, threshold: 90, posts: [1, 2, 3, 4, 5] } }
|
||||
end
|
||||
|
||||
trait :with_no_results do
|
||||
status { :no_results }
|
||||
search_results_count { 0 }
|
||||
download_time { 0.07 }
|
||||
image_processing_time { 0.03 }
|
||||
fingerprint_computation_time { 0.10 }
|
||||
search_computation_time { 0.05 }
|
||||
total_request_time { 0.25 }
|
||||
response_data { { matches: 0, threshold: 90 } }
|
||||
end
|
||||
|
||||
trait :with_error do
|
||||
status { :error }
|
||||
search_results_count { 0 }
|
||||
download_time { 0.05 }
|
||||
image_processing_time { nil }
|
||||
fingerprint_computation_time { nil }
|
||||
search_computation_time { nil }
|
||||
total_request_time { 0.15 }
|
||||
error_message { "Failed to process image: Invalid format" }
|
||||
response_data { { error: "Invalid image format" } }
|
||||
end
|
||||
|
||||
trait :invalid_image do
|
||||
status { :invalid_image }
|
||||
search_results_count { 0 }
|
||||
download_time { 0.04 }
|
||||
image_processing_time { nil }
|
||||
fingerprint_computation_time { nil }
|
||||
search_computation_time { nil }
|
||||
total_request_time { 0.12 }
|
||||
error_message { "Image format not supported" }
|
||||
response_data { { error: "Unsupported format" } }
|
||||
end
|
||||
|
||||
trait :minimal_user_info do
|
||||
telegram_username { nil }
|
||||
telegram_first_name { nil }
|
||||
telegram_last_name { nil }
|
||||
end
|
||||
|
||||
trait :username_only do
|
||||
telegram_first_name { nil }
|
||||
telegram_last_name { nil }
|
||||
telegram_username { "anonymous_user" }
|
||||
end
|
||||
|
||||
trait :slow_processing do
|
||||
download_time { 0.5 }
|
||||
image_processing_time { 0.3 }
|
||||
fingerprint_computation_time { 2.5 }
|
||||
search_computation_time { 1.8 }
|
||||
total_request_time { 5.1 }
|
||||
end
|
||||
|
||||
trait :fast_processing do
|
||||
download_time { 0.01 }
|
||||
image_processing_time { 0.005 }
|
||||
fingerprint_computation_time { 0.02 }
|
||||
search_computation_time { 0.01 }
|
||||
total_request_time { 0.045 }
|
||||
end
|
||||
end
|
||||
end
|
||||
108
spec/lib/stopwatch_spec.rb
Normal file
108
spec/lib/stopwatch_spec.rb
Normal file
@@ -0,0 +1,108 @@
|
||||
# typed: false
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Stopwatch do
|
||||
describe ".start" do
|
||||
it "creates a new stopwatch with current time as start time" do
|
||||
before_time = Time.current
|
||||
stopwatch = Stopwatch.start
|
||||
after_time = Time.current
|
||||
|
||||
expect(stopwatch.start_time).to be_between(before_time, after_time)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#elapsed" do
|
||||
it "calculates elapsed time correctly" do
|
||||
start_time = Time.parse("2023-01-01 10:00:00")
|
||||
stopwatch = Stopwatch.new(start_time)
|
||||
|
||||
# Mock Time.current for this specific stopwatch instance
|
||||
allow(stopwatch).to receive(:elapsed) { 2.5 }
|
||||
|
||||
expect(stopwatch.elapsed).to eq(2.5)
|
||||
end
|
||||
|
||||
it "returns non-negative elapsed time" do
|
||||
stopwatch = Stopwatch.start
|
||||
expect(stopwatch.elapsed).to be >= 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "#elapsed_ms" do
|
||||
it "formats milliseconds correctly" do
|
||||
stopwatch = Stopwatch.start
|
||||
allow(stopwatch).to receive(:elapsed) { 1.2345 }
|
||||
|
||||
expect(stopwatch.elapsed_ms).to eq("1234.5ms")
|
||||
end
|
||||
|
||||
it "handles very small times" do
|
||||
stopwatch = Stopwatch.start
|
||||
allow(stopwatch).to receive(:elapsed) { 0.001 }
|
||||
|
||||
expect(stopwatch.elapsed_ms).to eq("1.0ms")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#elapsed_s" do
|
||||
it "formats seconds with 3 decimal places" do
|
||||
stopwatch = Stopwatch.start
|
||||
allow(stopwatch).to receive(:elapsed) { 1.2345 }
|
||||
|
||||
expect(stopwatch.elapsed_s).to eq("1.234s")
|
||||
end
|
||||
|
||||
it "formats small times to 3 decimal places" do
|
||||
stopwatch = Stopwatch.start
|
||||
allow(stopwatch).to receive(:elapsed) { 0.001234 }
|
||||
|
||||
expect(stopwatch.elapsed_s).to eq("0.001s")
|
||||
end
|
||||
|
||||
it "formats zero time correctly" do
|
||||
stopwatch = Stopwatch.start
|
||||
allow(stopwatch).to receive(:elapsed) { 0.0 }
|
||||
|
||||
expect(stopwatch.elapsed_s).to eq("0.000s")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#start_time" do
|
||||
it "returns the time when the stopwatch was started" do
|
||||
start_time = Time.parse("2023-01-01 10:00:00")
|
||||
stopwatch = Stopwatch.new(start_time)
|
||||
|
||||
expect(stopwatch.start_time).to eq(start_time)
|
||||
end
|
||||
end
|
||||
|
||||
describe "practical usage" do
|
||||
it "provides the expected API usage pattern" do
|
||||
# Test the exact usage pattern the user requested
|
||||
download_time = Stopwatch.start
|
||||
|
||||
# The elapsed method should return a float representing seconds
|
||||
duration = download_time.elapsed
|
||||
expect(duration).to be_a(Float)
|
||||
expect(duration).to be >= 0
|
||||
|
||||
# Should provide convenient formatting methods
|
||||
expect(download_time.elapsed_s).to match(/^\d+\.\d{3}s$/)
|
||||
expect(download_time.elapsed_ms).to match(/^\d+\.\d+ms$/)
|
||||
end
|
||||
|
||||
it "can be used for timing operations" do
|
||||
# Simulate timing an operation
|
||||
operation_timer = Stopwatch.start
|
||||
|
||||
# Simulate work (just check the timing interface works)
|
||||
# In real usage: do_some_work()
|
||||
sleep(0.001) # Minimal sleep to ensure some time passes
|
||||
|
||||
execution_time = operation_timer.elapsed
|
||||
expect(execution_time).to be > 0
|
||||
expect(execution_time).to be < 1 # Should be very fast
|
||||
end
|
||||
end
|
||||
end
|
||||
466
spec/models/telegram_bot_log_spec.rb
Normal file
466
spec/models/telegram_bot_log_spec.rb
Normal file
@@ -0,0 +1,466 @@
|
||||
# typed: false
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe TelegramBotLog, type: :model do
|
||||
describe "validations" do
|
||||
subject { build(:telegram_bot_log) }
|
||||
|
||||
it { should validate_presence_of(:telegram_user_id) }
|
||||
it { should validate_presence_of(:telegram_chat_id) }
|
||||
it { should validate_presence_of(:request_timestamp) }
|
||||
it { should validate_presence_of(:status) }
|
||||
it { should validate_presence_of(:search_results_count) }
|
||||
|
||||
it do
|
||||
should validate_numericality_of(
|
||||
:search_results_count,
|
||||
).is_greater_than_or_equal_to(0)
|
||||
end
|
||||
|
||||
it do
|
||||
should validate_numericality_of(
|
||||
:fingerprint_computation_time,
|
||||
).is_greater_than(0).allow_nil
|
||||
end
|
||||
it do
|
||||
should validate_numericality_of(:search_computation_time).is_greater_than(
|
||||
0,
|
||||
).allow_nil
|
||||
end
|
||||
|
||||
it do
|
||||
should validate_length_of(:processed_image_sha256).is_equal_to(
|
||||
32,
|
||||
).allow_nil
|
||||
end
|
||||
|
||||
it do
|
||||
should define_enum_for(:status)
|
||||
.with_values(
|
||||
success: "success",
|
||||
error: "error",
|
||||
no_results: "no_results",
|
||||
invalid_image: "invalid_image",
|
||||
)
|
||||
.backed_by_column_of_type(:string)
|
||||
.with_prefix(:status)
|
||||
end
|
||||
|
||||
context "when fingerprint_computation_time is present" do
|
||||
it "is valid with a positive value" do
|
||||
subject.fingerprint_computation_time = 0.5
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
|
||||
it "is invalid with zero" do
|
||||
subject.fingerprint_computation_time = 0
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:fingerprint_computation_time]).to include(
|
||||
"must be greater than 0",
|
||||
)
|
||||
end
|
||||
|
||||
it "is invalid with negative value" do
|
||||
subject.fingerprint_computation_time = -0.1
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:fingerprint_computation_time]).to include(
|
||||
"must be greater than 0",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when search_computation_time is present" do
|
||||
it "is valid with a positive value" do
|
||||
subject.search_computation_time = 0.3
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
|
||||
it "is invalid with zero" do
|
||||
subject.search_computation_time = 0
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:search_computation_time]).to include(
|
||||
"must be greater than 0",
|
||||
)
|
||||
end
|
||||
|
||||
it "is invalid with negative value" do
|
||||
subject.search_computation_time = -0.1
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:search_computation_time]).to include(
|
||||
"must be greater than 0",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when download_time is present" do
|
||||
it "is valid with a positive value" do
|
||||
subject.download_time = 0.2
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
|
||||
it "is invalid with zero" do
|
||||
subject.download_time = 0
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:download_time]).to include(
|
||||
"must be greater than 0",
|
||||
)
|
||||
end
|
||||
|
||||
it "is invalid with negative value" do
|
||||
subject.download_time = -0.1
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:download_time]).to include(
|
||||
"must be greater than 0",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when image_processing_time is present" do
|
||||
it "is valid with a positive value" do
|
||||
subject.image_processing_time = 0.1
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
|
||||
it "is invalid with zero" do
|
||||
subject.image_processing_time = 0
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:image_processing_time]).to include(
|
||||
"must be greater than 0",
|
||||
)
|
||||
end
|
||||
|
||||
it "is invalid with negative value" do
|
||||
subject.image_processing_time = -0.05
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:image_processing_time]).to include(
|
||||
"must be greater than 0",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when total_request_time is present" do
|
||||
it "is valid with a positive value" do
|
||||
subject.total_request_time = 1.5
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
|
||||
it "is invalid with zero" do
|
||||
subject.total_request_time = 0
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:total_request_time]).to include(
|
||||
"must be greater than 0",
|
||||
)
|
||||
end
|
||||
|
||||
it "is invalid with negative value" do
|
||||
subject.total_request_time = -0.1
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:total_request_time]).to include(
|
||||
"must be greater than 0",
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "associations" do
|
||||
it { should belong_to(:processed_image).class_name("BlobFile").optional }
|
||||
|
||||
context "with processed image" do
|
||||
let(:blob_file) { create(:blob_file, :image_blob) }
|
||||
let(:log) { build(:telegram_bot_log, processed_image: blob_file) }
|
||||
|
||||
it "associates with BlobFile correctly" do
|
||||
expect(log.processed_image).to eq(blob_file)
|
||||
expect(log.processed_image_sha256).to eq(blob_file.sha256)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "scopes" do
|
||||
let!(:user1_log1) do
|
||||
create(:telegram_bot_log, telegram_user_id: 111, status: :success)
|
||||
end
|
||||
let!(:user1_log2) do
|
||||
create(:telegram_bot_log, telegram_user_id: 111, status: :error)
|
||||
end
|
||||
let!(:user2_log) do
|
||||
create(:telegram_bot_log, telegram_user_id: 222, status: :success)
|
||||
end
|
||||
let!(:no_results_log) { create(:telegram_bot_log, :with_no_results) }
|
||||
let!(:old_log) { create(:telegram_bot_log, request_timestamp: 1.week.ago) }
|
||||
let!(:recent_log) do
|
||||
create(:telegram_bot_log, request_timestamp: 1.hour.ago)
|
||||
end
|
||||
|
||||
describe ".for_user" do
|
||||
it "returns logs for specific user" do
|
||||
expect(TelegramBotLog.for_user(111)).to contain_exactly(
|
||||
user1_log1,
|
||||
user1_log2,
|
||||
)
|
||||
expect(TelegramBotLog.for_user(222)).to contain_exactly(user2_log)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".successful" do
|
||||
it "returns only successful logs" do
|
||||
successful_logs = TelegramBotLog.successful
|
||||
expect(successful_logs).to include(user1_log1, user2_log, recent_log)
|
||||
expect(successful_logs).not_to include(user1_log2, no_results_log)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".with_results" do
|
||||
it "returns logs with search results count > 0" do
|
||||
with_results = TelegramBotLog.with_results
|
||||
expect(with_results).not_to include(no_results_log)
|
||||
expect(with_results).to include(user1_log1, user2_log)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".recent" do
|
||||
it "orders by request_timestamp desc" do
|
||||
recent_logs = TelegramBotLog.recent
|
||||
expect(recent_logs.first.request_timestamp).to be >
|
||||
recent_logs.last.request_timestamp
|
||||
end
|
||||
end
|
||||
|
||||
describe ".by_date_range" do
|
||||
it "returns logs within date range" do
|
||||
start_date = 2.days.ago
|
||||
end_date = Time.current
|
||||
logs_in_range = TelegramBotLog.by_date_range(start_date, end_date)
|
||||
|
||||
expect(logs_in_range).to include(recent_log)
|
||||
expect(logs_in_range).not_to include(old_log)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "helper methods" do
|
||||
describe "#user_display_name" do
|
||||
context "with first and last name" do
|
||||
let(:log) do
|
||||
build(
|
||||
:telegram_bot_log,
|
||||
telegram_first_name: "John",
|
||||
telegram_last_name: "Doe",
|
||||
)
|
||||
end
|
||||
|
||||
it "returns full name" do
|
||||
expect(log.user_display_name).to eq("John Doe")
|
||||
end
|
||||
end
|
||||
|
||||
context "with only first name" do
|
||||
let(:log) do
|
||||
build(
|
||||
:telegram_bot_log,
|
||||
telegram_first_name: "John",
|
||||
telegram_last_name: nil,
|
||||
)
|
||||
end
|
||||
|
||||
it "returns first name" do
|
||||
expect(log.user_display_name).to eq("John")
|
||||
end
|
||||
end
|
||||
|
||||
context "with only username" do
|
||||
let(:log) do
|
||||
build(:telegram_bot_log, :username_only, telegram_username: "johndoe")
|
||||
end
|
||||
|
||||
it "returns username with @ prefix" do
|
||||
expect(log.user_display_name).to eq("@johndoe")
|
||||
end
|
||||
end
|
||||
|
||||
context "with minimal user info" do
|
||||
let(:log) do
|
||||
build(
|
||||
:telegram_bot_log,
|
||||
:minimal_user_info,
|
||||
telegram_user_id: 123_456,
|
||||
)
|
||||
end
|
||||
|
||||
it "returns user ID format" do
|
||||
expect(log.user_display_name).to eq("User 123456")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#has_performance_metrics?" do
|
||||
context "with core timing metrics" do
|
||||
let(:log) { build(:telegram_bot_log, :successful) }
|
||||
|
||||
it "returns true" do
|
||||
expect(log.has_performance_metrics?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context "with missing download timing" do
|
||||
let(:log) { build(:telegram_bot_log, download_time: nil) }
|
||||
|
||||
it "returns false" do
|
||||
expect(log.has_performance_metrics?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "with missing fingerprint timing" do
|
||||
let(:log) do
|
||||
build(:telegram_bot_log, fingerprint_computation_time: nil)
|
||||
end
|
||||
|
||||
it "returns false" do
|
||||
expect(log.has_performance_metrics?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "with missing search timing" do
|
||||
let(:log) { build(:telegram_bot_log, search_computation_time: nil) }
|
||||
|
||||
it "returns false" do
|
||||
expect(log.has_performance_metrics?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "with core timings missing" do
|
||||
let(:log) { build(:telegram_bot_log, :with_error) }
|
||||
|
||||
it "returns false" do
|
||||
expect(log.has_performance_metrics?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#has_complete_performance_metrics?" do
|
||||
context "with all timing metrics" do
|
||||
let(:log) { build(:telegram_bot_log, :successful) }
|
||||
|
||||
it "returns true" do
|
||||
expect(log.has_complete_performance_metrics?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context "with missing image_processing_time" do
|
||||
let(:log) { build(:telegram_bot_log, image_processing_time: nil) }
|
||||
|
||||
it "returns false" do
|
||||
expect(log.has_complete_performance_metrics?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "with missing any timing" do
|
||||
let(:log) { build(:telegram_bot_log, download_time: nil) }
|
||||
|
||||
it "returns false" do
|
||||
expect(log.has_complete_performance_metrics?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "database constraints and indexes" do
|
||||
it "has proper table name" do
|
||||
expect(TelegramBotLog.table_name).to eq("telegram_bot_logs")
|
||||
end
|
||||
|
||||
context "foreign key constraint" do
|
||||
let(:blob_file) { create(:blob_file, :image_blob) }
|
||||
|
||||
it "allows valid BlobFile association" do
|
||||
log = build(:telegram_bot_log, processed_image_sha256: blob_file.sha256)
|
||||
expect { log.save! }.not_to raise_error
|
||||
end
|
||||
|
||||
it "prevents invalid sha256 reference" do
|
||||
invalid_sha256 = "\x00" * 32 # Invalid sha256
|
||||
log = build(:telegram_bot_log, processed_image_sha256: invalid_sha256)
|
||||
expect { log.save! }.to raise_error(ActiveRecord::InvalidForeignKey)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "factory traits" do
|
||||
it "creates successful log with correct attributes" do
|
||||
log = build(:telegram_bot_log, :successful)
|
||||
expect(log.status).to eq("success")
|
||||
expect(log.search_results_count).to be > 0
|
||||
expect(log.has_performance_metrics?).to be true
|
||||
end
|
||||
|
||||
it "creates error log with correct attributes" do
|
||||
log = build(:telegram_bot_log, :with_error)
|
||||
expect(log.status).to eq("error")
|
||||
expect(log.search_results_count).to eq(0)
|
||||
expect(log.error_message).to be_present
|
||||
expect(log.has_performance_metrics?).to be false
|
||||
end
|
||||
|
||||
it "creates log with image association" do
|
||||
log = build(:telegram_bot_log, :with_image)
|
||||
expect(log.processed_image).to be_present
|
||||
expect(log.processed_image.content_type).to eq("image/jpeg")
|
||||
end
|
||||
|
||||
it "creates log with minimal user info" do
|
||||
log = build(:telegram_bot_log, :minimal_user_info)
|
||||
expect(log.telegram_username).to be_nil
|
||||
expect(log.telegram_first_name).to be_nil
|
||||
expect(log.telegram_last_name).to be_nil
|
||||
expect(log.user_display_name).to match(/User \d+/)
|
||||
end
|
||||
end
|
||||
|
||||
describe "realistic usage scenarios" do
|
||||
context "successful image search" do
|
||||
let(:blob_file) { create(:blob_file, :image_blob) }
|
||||
let(:log) do
|
||||
create(
|
||||
:telegram_bot_log,
|
||||
:successful,
|
||||
:with_image,
|
||||
processed_image: blob_file,
|
||||
response_data: {
|
||||
matches: 3,
|
||||
threshold: 90,
|
||||
posts: [
|
||||
{ id: 1, similarity: 95.2, url: "https://example.com/1" },
|
||||
{ id: 2, similarity: 92.1, url: "https://example.com/2" },
|
||||
{ id: 3, similarity: 90.5, url: "https://example.com/3" },
|
||||
],
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it "saves and retrieves correctly" do
|
||||
expect(log).to be_persisted
|
||||
expect(log.status_success?).to be true
|
||||
expect(log.processed_image).to eq(blob_file)
|
||||
expect(log.response_data["matches"]).to eq(3)
|
||||
expect(log.total_request_time).to be > 0
|
||||
end
|
||||
end
|
||||
|
||||
context "failed image processing" do
|
||||
let(:log) do
|
||||
create(
|
||||
:telegram_bot_log,
|
||||
:with_error,
|
||||
error_message: "Corrupted image file",
|
||||
)
|
||||
end
|
||||
|
||||
it "properly records error state" do
|
||||
expect(log.status_error?).to be true
|
||||
expect(log.search_results_count).to eq(0)
|
||||
expect(log.error_message).to eq("Corrupted image file")
|
||||
expect(log.has_performance_metrics?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
68
spec/policies/telegram_bot_log_policy_spec.rb
Normal file
68
spec/policies/telegram_bot_log_policy_spec.rb
Normal file
@@ -0,0 +1,68 @@
|
||||
# typed: false
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe TelegramBotLogPolicy, type: :policy do
|
||||
let(:admin_user) { create(:user, role: :admin) }
|
||||
let(:regular_user) { create(:user, role: :user) }
|
||||
let(:telegram_bot_log) { create(:telegram_bot_log, :successful) }
|
||||
|
||||
describe "permissions" do
|
||||
describe "index?" do
|
||||
it "grants access for admin users" do
|
||||
policy = TelegramBotLogPolicy.new(admin_user, TelegramBotLog)
|
||||
expect(policy.index?).to be true
|
||||
end
|
||||
|
||||
it "denies access for regular users" do
|
||||
policy = TelegramBotLogPolicy.new(regular_user, TelegramBotLog)
|
||||
expect(policy.index?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe "show?" do
|
||||
it "grants access for admin users" do
|
||||
policy = TelegramBotLogPolicy.new(admin_user, telegram_bot_log)
|
||||
expect(policy.show?).to be true
|
||||
end
|
||||
|
||||
it "denies access for regular users" do
|
||||
policy = TelegramBotLogPolicy.new(regular_user, telegram_bot_log)
|
||||
expect(policy.show?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe "other actions" do
|
||||
it "denies new/create/edit/update/destroy for all users" do
|
||||
admin_policy = TelegramBotLogPolicy.new(admin_user, telegram_bot_log)
|
||||
regular_policy =
|
||||
TelegramBotLogPolicy.new(regular_user, telegram_bot_log)
|
||||
|
||||
# These methods are inherited from ApplicationPolicy and should be false
|
||||
expect(admin_policy.create?).to be false
|
||||
expect(admin_policy.new?).to be false
|
||||
expect(admin_policy.edit?).to be false
|
||||
expect(admin_policy.update?).to be false
|
||||
expect(admin_policy.destroy?).to be false
|
||||
|
||||
expect(regular_policy.create?).to be false
|
||||
expect(regular_policy.new?).to be false
|
||||
expect(regular_policy.edit?).to be false
|
||||
expect(regular_policy.update?).to be false
|
||||
expect(regular_policy.destroy?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "scope" do
|
||||
let!(:log1) { create(:telegram_bot_log, :successful) }
|
||||
let!(:log2) { create(:telegram_bot_log, :with_error) }
|
||||
|
||||
it "returns all logs for admin users" do
|
||||
scope = Pundit.policy_scope(admin_user, TelegramBotLog)
|
||||
expect(scope).to include(log1, log2)
|
||||
end
|
||||
|
||||
# Note: The policy scope returns scope.all for all users, but actual access control
|
||||
# is handled at the controller level via authorize calls
|
||||
end
|
||||
end
|
||||
31
test/fixtures/telegram_bot_logs.yml
vendored
Normal file
31
test/fixtures/telegram_bot_logs.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
one:
|
||||
telegram_user_id:
|
||||
telegram_username: MyString
|
||||
telegram_first_name: MyString
|
||||
telegram_last_name: MyString
|
||||
telegram_chat_id:
|
||||
request_timestamp: 2025-07-31 03:55:48
|
||||
fingerprint_computation_time: 1.5
|
||||
search_computation_time: 1.5
|
||||
search_results_count: 1
|
||||
response_data:
|
||||
status: MyString
|
||||
error_message: MyText
|
||||
processed_image_sha256:
|
||||
|
||||
two:
|
||||
telegram_user_id:
|
||||
telegram_username: MyString
|
||||
telegram_first_name: MyString
|
||||
telegram_last_name: MyString
|
||||
telegram_chat_id:
|
||||
request_timestamp: 2025-07-31 03:55:48
|
||||
fingerprint_computation_time: 1.5
|
||||
search_computation_time: 1.5
|
||||
search_results_count: 1
|
||||
response_data:
|
||||
status: MyString
|
||||
error_message: MyText
|
||||
processed_image_sha256:
|
||||
7
test/models/telegram_bot_log_test.rb
Normal file
7
test/models/telegram_bot_log_test.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
require "test_helper"
|
||||
|
||||
class TelegramBotLogTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
Reference in New Issue
Block a user