This commit is contained in:
CroneKorkN 2026-01-18 18:50:31 +01:00
parent 7022b48bda
commit 34be944b96
Signed by: cronekorkn
SSH key fingerprint: SHA256:v0410ZKfuO1QHdgKBsdQNF64xmTxOF8osF1LIqwTcVw
16 changed files with 333 additions and 2 deletions

View file

@ -0,0 +1,32 @@
class JobLogsController < ApplicationController
before_action :set_job_log, only: [ :show ]
def index
@job_logs = if params[:server_id]
server = current_user.servers.find(params[:server_id])
server.job_logs.recent
else
JobLog.where(server_id: current_user.servers.pluck(:id))
.or(JobLog.where(server_id: nil))
.recent
end
@job_logs = @job_logs.page(params[:page]).per(20) if defined?(Kaminari)
end
def show
end
private
def set_job_log
@job_log = JobLog.find(params[:id])
# Authorize: user must own the server or job must be global (no server)
unless @job_log.server_id.nil? || current_user.servers.exists?(id: @job_log.server_id)
redirect_to job_logs_path, alert: "Not authorized"
end
end
def authorize_user!
redirect_to root_path, alert: "Please log in" unless current_user
end
end

View file

@ -0,0 +1,2 @@
module JobLogsHelper
end

View file

@ -4,4 +4,54 @@ class ApplicationJob < ActiveJob::Base
# Most jobs are safe to ignore if the underlying records are no longer available # Most jobs are safe to ignore if the underlying records are no longer available
# discard_on ActiveJob::DeserializationError # discard_on ActiveJob::DeserializationError
around_perform do |_job, block|
log_entry = create_job_log
@job_log = log_entry
begin
log_entry.update!(status: :running, started_at: Time.current)
log("Job started: #{self.class.name}")
block.call
log("Job completed successfully")
log_entry.update!(status: :completed, finished_at: Time.current)
rescue => e
log("Job failed: #{e.class.name} - #{e.message}")
log(e.backtrace.first(5).join("\n"))
log_entry.update!(
status: :failed,
finished_at: Time.current,
error_message: "#{e.class.name}: #{e.message}"
)
raise
end
end
private
def create_job_log
server_id = extract_server_id_from_arguments
JobLog.create!(
job_class: self.class.name,
job_id: job_id,
arguments: arguments.to_json,
server_id: server_id,
status: :pending
)
end
def extract_server_id_from_arguments
# First argument is typically server_id for server-related jobs
return nil if arguments.empty?
arg = arguments.first
arg.is_a?(Integer) ? arg : nil
end
def log(message)
return unless @job_log
@job_log.append_log(message)
end
end end

View file

@ -3,19 +3,32 @@ class SpawnServerJob < ApplicationJob
def perform(server_id) def perform(server_id)
server = Server.find(server_id) server = Server.find(server_id)
log "Found server: #{server.name}"
begin begin
log "Starting server spawn process..."
L4dServer::Launcher.spawn(server) L4dServer::Launcher.spawn(server)
log "Server spawned successfully"
server.update(status: :starting) server.update(status: :starting)
log "Server status updated to starting"
# Generate a random RCON password for health checks # Generate a random RCON password for health checks
server.update(rcon_password: SecureRandom.hex(16)) rcon_password = SecureRandom.hex(16)
server.update(rcon_password: rcon_password)
log "RCON password generated and saved"
Activity.log(server.user, "spawned_success", "Server", server.id, { name: server.name }) Activity.log(server.user, "spawned_success", "Server", server.id, { name: server.name })
log "Activity logged"
rescue StandardError => e rescue StandardError => e
log "ERROR: Server spawn failed: #{e.message}"
log "Backtrace: #{e.backtrace.first(3).join("\n")}"
Rails.logger.error("Server spawn failed: #{e.message}") Rails.logger.error("Server spawn failed: #{e.message}")
server.update(status: :failed) server.update(status: :failed)
Activity.log(server.user, "spawn_failed", "Server", server.id, { error: e.message }) Activity.log(server.user, "spawn_failed", "Server", server.id, { error: e.message })
raise
end end
end end
end end

19
app/models/job_log.rb Normal file
View file

@ -0,0 +1,19 @@
class JobLog < ApplicationRecord
belongs_to :server, optional: true
enum :status, { pending: 0, running: 1, completed: 2, failed: 3 }
scope :recent, -> { order(created_at: :desc) }
scope :for_server, ->(server_id) { where(server_id: server_id) }
def duration
return nil unless started_at && finished_at
finished_at - started_at
end
def append_log(message)
self.log_output ||= ""
self.log_output += "[#{Time.current.strftime('%Y-%m-%d %H:%M:%S')}] #{message}\n"
save
end
end

View file

@ -1,6 +1,7 @@
class Server < ApplicationRecord class Server < ApplicationRecord
belongs_to :user belongs_to :user
belongs_to :server_template belongs_to :server_template
has_many :job_logs, dependent: :destroy
enum :status, { stopped: 0, starting: 1, running: 2, failed: 3 } enum :status, { stopped: 0, starting: 1, running: 2, failed: 3 }

View file

@ -0,0 +1,47 @@
h1 Job Logs
- if params[:server_id]
p
= link_to "← Back to Server", server_path(params[:server_id]), class: "btn btn--secondary"
= link_to "View All Jobs", job_logs_path, class: "btn btn--secondary"
- else
p.text-muted Showing all job logs for your servers
- if @job_logs.any?
table.table
thead
tr
th Job Class
th Server
th Status
th Started
th Duration
th Actions
tbody
- @job_logs.each do |job_log|
tr class="job-status-#{job_log.status}"
td= job_log.job_class
td
- if job_log.server
= link_to job_log.server.name, server_path(job_log.server)
- else
span.text-muted System
td
span class="badge badge--#{job_log.status}"
= job_log.status.titleize
td
- if job_log.started_at
= job_log.started_at.strftime("%Y-%m-%d %H:%M:%S")
- else
span.text-muted Pending
td
- if job_log.duration
= "#{job_log.duration.round(2)}s"
- elsif job_log.running?
span.text-muted Running...
- else
span.text-muted -
td
= link_to "View Log", job_log_path(job_log), class: "btn btn--small"
- else
p.text-muted No job logs found.

View file

@ -0,0 +1,56 @@
h1 Job Log Details
.job-log-header
.job-info
h2= @job_log.job_class
dl.info-grid
dt Status:
dd
span class="badge badge--#{@job_log.status}"
= @job_log.status.titleize
- if @job_log.server
dt Server:
dd= link_to @job_log.server.name, server_path(@job_log.server)
dt Job ID:
dd= @job_log.job_id || "N/A"
dt Started:
dd
- if @job_log.started_at
= @job_log.started_at.strftime("%Y-%m-%d %H:%M:%S")
- else
span.text-muted Not started
dt Finished:
dd
- if @job_log.finished_at
= @job_log.finished_at.strftime("%Y-%m-%d %H:%M:%S")
- else
span.text-muted -
- if @job_log.duration
dt Duration:
dd= "#{@job_log.duration.round(2)} seconds"
- if @job_log.error_message.present?
dt Error:
dd.error-message= @job_log.error_message
.job-arguments
h3 Arguments
pre.code-block= JSON.pretty_generate(JSON.parse(@job_log.arguments)) rescue @job_log.arguments
.job-log-output
h3 Log Output
- if @job_log.log_output.present?
pre.log-output= @job_log.log_output
- else
p.text-muted No log output available.
.actions
= link_to "← Back to Jobs", @job_log.server ? job_logs_path(server_id: @job_log.server_id) : job_logs_path, class: "btn btn--secondary"
- if @job_log.server
= link_to "View Server", server_path(@job_log.server), class: "btn btn--secondary"

View file

@ -28,6 +28,7 @@ html
= link_to "Servers", servers_path = link_to "Servers", servers_path
= link_to "Templates", server_templates_path = link_to "Templates", server_templates_path
= link_to "Overlays", overlays_path = link_to "Overlays", overlays_path
= link_to "Jobs", job_logs_path
span style="margin-left:auto;" span style="margin-left:auto;"
= link_to "Logout", logout_path = link_to "Logout", logout_path

View file

@ -28,6 +28,31 @@
= button_to "Delete Server", server_path(@server), method: :delete, data: { confirm: "Are you sure? This will stop and remove the server." }, class: "btn btn--danger" = button_to "Delete Server", server_path(@server), method: :delete, data: { confirm: "Are you sure? This will stop and remove the server." }, class: "btn btn--danger"
= link_to "Back to Servers", servers_path, class: "btn" = link_to "Back to Servers", servers_path, class: "btn"
section.job-logs
h3 Job History
- if @server.job_logs.recent.limit(5).any?
table.table.table--compact
thead
tr
th Job
th Status
th Started
th Duration
th Actions
tbody
- @server.job_logs.recent.limit(5).each do |job_log|
tr
td= job_log.job_class
td
span class="badge badge--#{job_log.status}"
= job_log.status.titleize
td= job_log.started_at&.strftime("%H:%M:%S") || "-"
td= job_log.duration ? "#{job_log.duration.round(1)}s" : "-"
td= link_to "View", job_log_path(job_log), class: "btn btn--small"
p= link_to "View All Jobs →", job_logs_path(server_id: @server.id), class: "link"
- else
p.text-muted No jobs executed yet.
section.logs section.logs
h3 Live Logs h3 Live Logs
#log-output #log-output

View file

@ -17,6 +17,8 @@ Rails.application.routes.draw do
resources :overlays, only: [ :index, :new, :create, :destroy ] resources :overlays, only: [ :index, :new, :create, :destroy ]
resources :job_logs, only: [ :index, :show ]
resources :servers do resources :servers do
member do member do
post :spawn post :spawn

View file

@ -0,0 +1,22 @@
class CreateJobLogs < ActiveRecord::Migration[8.1]
def change
create_table :job_logs do |t|
t.string :job_class
t.string :job_id
t.text :arguments
t.integer :server_id
t.text :log_output
t.integer :status, default: 0, null: false
t.datetime :started_at
t.datetime :finished_at
t.text :error_message
t.timestamps
end
add_index :job_logs, :server_id
add_index :job_logs, :job_id
add_index :job_logs, :status
add_index :job_logs, :created_at
end
end

20
db/schema.rb generated
View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2026_01_18_200000) do ActiveRecord::Schema[8.1].define(version: 2026_01_18_200001) do
create_table "activities", force: :cascade do |t| create_table "activities", force: :cascade do |t|
t.string "action", null: false t.string "action", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
@ -23,6 +23,24 @@ ActiveRecord::Schema[8.1].define(version: 2026_01_18_200000) do
t.index ["user_id"], name: "index_activities_on_user_id" t.index ["user_id"], name: "index_activities_on_user_id"
end end
create_table "job_logs", force: :cascade do |t|
t.text "arguments"
t.datetime "created_at", null: false
t.text "error_message"
t.datetime "finished_at"
t.string "job_class"
t.string "job_id"
t.text "log_output"
t.integer "server_id"
t.datetime "started_at"
t.integer "status", default: 0, null: false
t.datetime "updated_at", null: false
t.index ["created_at"], name: "index_job_logs_on_created_at"
t.index ["job_id"], name: "index_job_logs_on_job_id"
t.index ["server_id"], name: "index_job_logs_on_server_id"
t.index ["status"], name: "index_job_logs_on_status"
end
create_table "overlays", force: :cascade do |t| create_table "overlays", force: :cascade do |t|
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.text "description" t.text "description"

View file

@ -0,0 +1,13 @@
require "test_helper"
class JobLogsControllerTest < ActionDispatch::IntegrationTest
test "should get index" do
get job_logs_index_url
assert_response :success
end
test "should get show" do
get job_logs_show_url
assert_response :success
end
end

23
test/fixtures/job_logs.yml vendored Normal file
View file

@ -0,0 +1,23 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
job_class: MyString
job_id: MyString
arguments: MyText
server_id: 1
log_output: MyText
status: 1
started_at: 2026-01-18 18:46:23
finished_at: 2026-01-18 18:46:23
error_message: MyText
two:
job_class: MyString
job_id: MyString
arguments: MyText
server_id: 1
log_output: MyText
status: 1
started_at: 2026-01-18 18:46:23
finished_at: 2026-01-18 18:46:23
error_message: MyText

View file

@ -0,0 +1,7 @@
require "test_helper"
class JobLogTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end