diff --git a/app/controllers/job_logs_controller.rb b/app/controllers/job_logs_controller.rb new file mode 100644 index 0000000..5909f37 --- /dev/null +++ b/app/controllers/job_logs_controller.rb @@ -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 diff --git a/app/helpers/job_logs_helper.rb b/app/helpers/job_logs_helper.rb new file mode 100644 index 0000000..73db640 --- /dev/null +++ b/app/helpers/job_logs_helper.rb @@ -0,0 +1,2 @@ +module JobLogsHelper +end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index d394c3d..a0dd2cc 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -4,4 +4,54 @@ class ApplicationJob < ActiveJob::Base # Most jobs are safe to ignore if the underlying records are no longer available # 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 diff --git a/app/jobs/spawn_server_job.rb b/app/jobs/spawn_server_job.rb index cedb28d..c3635cc 100644 --- a/app/jobs/spawn_server_job.rb +++ b/app/jobs/spawn_server_job.rb @@ -3,19 +3,32 @@ class SpawnServerJob < ApplicationJob def perform(server_id) server = Server.find(server_id) + log "Found server: #{server.name}" begin + log "Starting server spawn process..." L4dServer::Launcher.spawn(server) + log "Server spawned successfully" + server.update(status: :starting) + log "Server status updated to starting" # 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 }) + log "Activity logged" 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}") server.update(status: :failed) Activity.log(server.user, "spawn_failed", "Server", server.id, { error: e.message }) + + raise end end end diff --git a/app/models/job_log.rb b/app/models/job_log.rb new file mode 100644 index 0000000..8a0d919 --- /dev/null +++ b/app/models/job_log.rb @@ -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 diff --git a/app/models/server.rb b/app/models/server.rb index 9ae5f6f..e679b1e 100644 --- a/app/models/server.rb +++ b/app/models/server.rb @@ -1,6 +1,7 @@ class Server < ApplicationRecord belongs_to :user belongs_to :server_template + has_many :job_logs, dependent: :destroy enum :status, { stopped: 0, starting: 1, running: 2, failed: 3 } diff --git a/app/views/job_logs/index.html.slim b/app/views/job_logs/index.html.slim new file mode 100644 index 0000000..acbae85 --- /dev/null +++ b/app/views/job_logs/index.html.slim @@ -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. diff --git a/app/views/job_logs/show.html.slim b/app/views/job_logs/show.html.slim new file mode 100644 index 0000000..df1ed5c --- /dev/null +++ b/app/views/job_logs/show.html.slim @@ -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" diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index a0aff74..b20bee0 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -28,6 +28,7 @@ html = link_to "Servers", servers_path = link_to "Templates", server_templates_path = link_to "Overlays", overlays_path + = link_to "Jobs", job_logs_path span style="margin-left:auto;" = link_to "Logout", logout_path diff --git a/app/views/servers/show.html.slim b/app/views/servers/show.html.slim index 7a489ec..f086e51 100644 --- a/app/views/servers/show.html.slim +++ b/app/views/servers/show.html.slim @@ -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" = 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 h3 Live Logs #log-output diff --git a/config/routes.rb b/config/routes.rb index bf2e616..7a63742 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -17,6 +17,8 @@ Rails.application.routes.draw do resources :overlays, only: [ :index, :new, :create, :destroy ] + resources :job_logs, only: [ :index, :show ] + resources :servers do member do post :spawn diff --git a/db/migrate/20260118200001_create_job_logs.rb b/db/migrate/20260118200001_create_job_logs.rb new file mode 100644 index 0000000..d0e5656 --- /dev/null +++ b/db/migrate/20260118200001_create_job_logs.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 6528063..1a6800f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # 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| t.string "action", 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" 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| t.datetime "created_at", null: false t.text "description" diff --git a/test/controllers/job_logs_controller_test.rb b/test/controllers/job_logs_controller_test.rb new file mode 100644 index 0000000..09c2e06 --- /dev/null +++ b/test/controllers/job_logs_controller_test.rb @@ -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 diff --git a/test/fixtures/job_logs.yml b/test/fixtures/job_logs.yml new file mode 100644 index 0000000..fea3be3 --- /dev/null +++ b/test/fixtures/job_logs.yml @@ -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 diff --git a/test/models/job_log_test.rb b/test/models/job_log_test.rb new file mode 100644 index 0000000..8b7c6b0 --- /dev/null +++ b/test/models/job_log_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class JobLogTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end