rake_audit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +38 -0
  3. data/LICENSE +7 -0
  4. data/README.md +235 -0
  5. data/app/controllers/rake_audit/application_controller.rb +53 -0
  6. data/app/controllers/rake_audit/dashboard_controller.rb +36 -0
  7. data/app/controllers/rake_audit/executions_controller.rb +39 -0
  8. data/app/models/rake_audit/task_execution.rb +33 -0
  9. data/app/views/layouts/rake_audit/application.html.erb +70 -0
  10. data/app/views/rake_audit/dashboard/index.html.erb +48 -0
  11. data/app/views/rake_audit/executions/index.html.erb +74 -0
  12. data/app/views/rake_audit/executions/show.html.erb +46 -0
  13. data/config/routes.rb +13 -0
  14. data/db/migrate/20260531120000_create_rake_task_executions.rb +62 -0
  15. data/lib/generators/rake_audit/install_generator.rb +42 -0
  16. data/lib/generators/rake_audit/templates/create_rake_task_executions.rb +55 -0
  17. data/lib/generators/rake_audit/templates/initializer.rb +17 -0
  18. data/lib/rake_audit/adapters/active_record_adapter.rb +73 -0
  19. data/lib/rake_audit/adapters/base.rb +94 -0
  20. data/lib/rake_audit/adapters/execution_record.rb +20 -0
  21. data/lib/rake_audit/adapters/mongo_adapter.rb +137 -0
  22. data/lib/rake_audit/adapters/redis_adapter.rb +174 -0
  23. data/lib/rake_audit/builders/task_execution_record_builder.rb +81 -0
  24. data/lib/rake_audit/configuration.rb +62 -0
  25. data/lib/rake_audit/execution_recorder.rb +78 -0
  26. data/lib/rake_audit/rails/engine.rb +20 -0
  27. data/lib/rake_audit/rails/railtie.rb +17 -0
  28. data/lib/rake_audit/record_not_found.rb +5 -0
  29. data/lib/rake_audit/task_execution_record.rb +46 -0
  30. data/lib/rake_audit/task_patch.rb +19 -0
  31. data/lib/rake_audit/version.rb +5 -0
  32. data/lib/rake_audit.rb +82 -0
  33. metadata +120 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9a179a34a78178c7a695d637e8ff9479d016258a2d7d69cb62e3b8ac33b557b7
4
+ data.tar.gz: 6c8cce01a8857dd1d3e50d32bdffe0a2f8d603460018d1aa70d59e01367496f4
5
+ SHA512:
6
+ metadata.gz: 50022c1e1011ad9143446c28f3b23a4563d1fc47251985e6bb81070f3f39793f898f42a83ffaf85d22cfad256fbbcef7e03b3e8fc163733fedaedabd4d80b620
7
+ data.tar.gz: 165a918cfeccc962b3f86342e3306b6efb8c54c7891103d674527192760442469888e553ffc38ec11f83e76c8a69433197bd7a1db2c057ef65f551d360fa9bfe
data/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-06-03
11
+
12
+ Initial public release.
13
+
14
+ ### Added
15
+
16
+ - **Automatic Rake auditing** — a prepended `Rake::Task#execute` hook records the
17
+ task name, arguments, start/finish times, duration, status (success/failure), and
18
+ error details for every Rake task execution, without altering task behavior.
19
+ - **Pluggable storage adapters** built on a common `RakeAudit::Adapters::Base`:
20
+ - `ActiveRecordAdapter` — persists executions to a `rake_task_executions` table.
21
+ - `RedisAdapter` — stores executions as JSON in the `rake_audit:executions` list.
22
+ - `MongoAdapter` — stores executions in the `rake_task_executions` collection.
23
+ - Custom adapters are supported by subclassing `Base` and implementing `#save`.
24
+ - **Configurable capture** of hostname, PID, Ruby version, and Rails environment,
25
+ plus a configurable logger for adapter save failures.
26
+ - **Rails Web UI** (mountable engine) providing:
27
+ - A paginated, filterable execution list (Kaminari-backed).
28
+ - A dashboard with aggregate stats (totals, success/failure counts, average
29
+ duration, top failed tasks).
30
+ - A per-execution detail view.
31
+ - Optional access control via a configurable `authenticate_with` lambda.
32
+ - **Rails install generator** (`rails generate rake_audit:install`) that copies an
33
+ initializer and a timestamped ActiveRecord migration.
34
+ - **Rails-free operation** — the gem loads and records outside Rails; Kaminari's
35
+ ActiveRecord/ActionView integrations stay inert until those libraries are present.
36
+
37
+ [Unreleased]: https://github.com/eraxel-dev/rake_audit/compare/v0.1.0...HEAD
38
+ [0.1.0]: https://github.com/eraxel-dev/rake_audit/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2026 Eraxel Dev
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # Rake Audit
2
+
3
+ **Full Audit Trail for Every Rake Execution**
4
+
5
+ RakeAudit automatically records who ran what, when it started, when it finished, how long it took, and whether it succeeded or failed.
6
+
7
+ Built for modern Rails applications, it supports pluggable storage adapters including ActiveRecord, Redis, MongoDB, and custom backends.
8
+
9
+ Gain a complete historical record of your operational tasks without touching your existing codebase.
10
+
11
+ ## Table of Contents
12
+
13
+ - [Installation](#installation)
14
+ - [Setup](#setup)
15
+ - [Rails (ActiveRecord)](#rails-activerecord)
16
+ - [Redis](#redis)
17
+ - [MongoDB](#mongodb)
18
+ - [Plain Ruby / Standalone Rake](#plain-ruby--standalone-rake)
19
+ - [Configuration](#configuration)
20
+ - [Web UI](#web-ui)
21
+ - [Custom Adapter](#custom-adapter)
22
+ - [Example Usages](#example-usages)
23
+
24
+ ---
25
+
26
+ ## Installation
27
+
28
+ Add the gem to your `Gemfile`:
29
+
30
+ ```ruby
31
+ gem 'rake_audit'
32
+ ```
33
+
34
+ Then run:
35
+
36
+ ```sh
37
+ bundle install
38
+ ```
39
+
40
+ Or install directly:
41
+
42
+ ```sh
43
+ gem install rake_audit
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Setup
49
+
50
+ ### Rails (ActiveRecord)
51
+
52
+ Run the install generator to create the initializer and database migration:
53
+
54
+ ```sh
55
+ rails generate rake_audit:install
56
+ ```
57
+
58
+ This copies `config/initializers/rake_audit.rb` and a timestamped migration to `db/migrate/`. Run the migration:
59
+
60
+ ```sh
61
+ rails db:migrate
62
+ ```
63
+
64
+ Then enable the ActiveRecord adapter in `config/initializers/rake_audit.rb`:
65
+
66
+ ```ruby
67
+ RakeAudit.configure do |config|
68
+ config.adapter = RakeAudit::Adapters::ActiveRecordAdapter.new
69
+ end
70
+ ```
71
+
72
+ ### Redis
73
+
74
+ Provide a connected `Redis` client:
75
+
76
+ ```ruby
77
+ require 'redis'
78
+
79
+ RakeAudit.configure do |config|
80
+ config.adapter = RakeAudit::Adapters::RedisAdapter.new(
81
+ client: Redis.new(url: ENV['REDIS_URL'])
82
+ )
83
+ end
84
+ ```
85
+
86
+ Execution records are stored as JSON in the `rake_audit:executions` list key.
87
+
88
+ ### MongoDB
89
+
90
+ Provide a connected `Mongo::Client`:
91
+
92
+ ```ruby
93
+ require 'mongo'
94
+
95
+ RakeAudit.configure do |config|
96
+ config.adapter = RakeAudit::Adapters::MongoAdapter.new(
97
+ client: Mongo::Client.new(ENV['MONGODB_URI'])
98
+ )
99
+ end
100
+ ```
101
+
102
+ Records are stored in the `rake_task_executions` collection.
103
+
104
+ ### Plain Ruby / Standalone Rake
105
+
106
+ RakeAudit works outside Rails. Require the gem and configure an adapter before tasks run:
107
+
108
+ ```ruby
109
+ require 'rake_audit'
110
+
111
+ RakeAudit.configure do |config|
112
+ config.adapter = MyCustomAdapter.new
113
+ end
114
+
115
+ RakeAudit.install!
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Configuration
121
+
122
+ All options with their defaults:
123
+
124
+ ```ruby
125
+ RakeAudit.configure do |config|
126
+ # Storage adapter — required to enable recording.
127
+ config.adapter = RakeAudit::Adapters::ActiveRecordAdapter.new
128
+
129
+ # Logger for adapter save failures (defaults to Rails.logger or $stdout).
130
+ config.logger = Rails.logger
131
+
132
+ # Fields captured on each execution record.
133
+ config.capture_hostname = true # machine hostname
134
+ config.capture_pid = true # process ID
135
+ config.capture_ruby_version = true # Ruby version string
136
+ config.capture_rails_env = true # Rails.env value
137
+
138
+ # Web UI (see below).
139
+ config.web_ui_enabled = true
140
+ config.authenticate_with = ->(controller) { controller.authenticate_admin! }
141
+ end
142
+ ```
143
+
144
+ Without an adapter, RakeAudit loads silently and records nothing.
145
+
146
+ ---
147
+
148
+ ## Web UI
149
+
150
+ Mount the engine in `config/routes.rb` to enable the built-in dashboard and execution list:
151
+
152
+ ```ruby
153
+ Rails.application.routes.draw do
154
+ mount RakeAudit::Engine, at: '/rake_audit'
155
+ end
156
+ ```
157
+
158
+ | Path | Description |
159
+ |---|---|
160
+ | `/rake_audit` | Paginated execution list with filters |
161
+ | `/rake_audit/dashboard` | Aggregate stats (total, success/failure counts, avg duration) |
162
+ | `/rake_audit/executions/:id` | Detail view for a single execution |
163
+
164
+ To restrict access, set `authenticate_with` to a lambda that calls your authentication helper:
165
+
166
+ ```ruby
167
+ config.authenticate_with = ->(controller) { controller.authenticate_admin! }
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Custom Adapter
173
+
174
+ Subclass `RakeAudit::Adapters::Base` and implement `#save`. Implement the remaining query methods to support the Web UI:
175
+
176
+ ```ruby
177
+ class MyAdapter < RakeAudit::Adapters::Base
178
+ def save(record)
179
+ # record is a RakeAudit::TaskExecutionRecord
180
+ # record.to_h returns a plain hash of all fields
181
+ MyStore.insert(record.to_h)
182
+ end
183
+
184
+ # Optional — required only when using the Web UI.
185
+ def query(filters: {}, page: nil, per_page: 25) = raise NotImplementedError
186
+ def find(id) = raise NotImplementedError
187
+ def count = raise NotImplementedError
188
+ def count_by_status(status) = raise NotImplementedError
189
+ def average_duration_ms = raise NotImplementedError
190
+ def top_failed_tasks(limit: 10) = raise NotImplementedError
191
+ end
192
+
193
+ RakeAudit.configure { |c| c.adapter = MyAdapter.new }
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Example Usages
199
+
200
+ **Query executions directly (ActiveRecord adapter)**
201
+
202
+ ```ruby
203
+ # All executions for a task, newest first.
204
+ RakeAudit::TaskExecution.where(task_name: 'db:migrate').order(started_at: :desc)
205
+
206
+ # Failed executions in the last 24 hours.
207
+ RakeAudit::TaskExecution.where(status: 'failure')
208
+ .where(started_at: 24.hours.ago..)
209
+ ```
210
+
211
+ **Check stats programmatically**
212
+
213
+ ```ruby
214
+ adapter = RakeAudit.config.adapter
215
+ stats = adapter.stats
216
+ # => { total: 1482, success_count: 1470, failure_count: 12,
217
+ # average_duration_ms: 843.2, top_failed_tasks: [["db:seed", 7], ...] }
218
+ ```
219
+
220
+ **Reset configuration in tests**
221
+
222
+ ```ruby
223
+ RSpec.configure do |config|
224
+ config.after { RakeAudit.reset_config! }
225
+ end
226
+ ```
227
+
228
+ **Disable the Web UI for API-only apps**
229
+
230
+ ```ruby
231
+ RakeAudit.configure do |config|
232
+ config.adapter = RakeAudit::Adapters::ActiveRecordAdapter.new
233
+ config.web_ui_enabled = false
234
+ end
235
+ ```
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RakeAudit
4
+ # Base controller for every RakeAudit Web UI page.
5
+ #
6
+ # It installs the pluggable authentication hook as a +before_action+. When the
7
+ # host application has configured {RakeAudit::Configuration#authenticate_with}
8
+ # with a callable, that callable is run in the controller instance's context on
9
+ # every request (so it may call host helpers such as +authenticate_admin!+ or
10
+ # +redirect_to+). When +authenticate_with+ is +nil+, the hook is a no-op and
11
+ # all pages are publicly accessible.
12
+ class ApplicationController < ActionController::Base
13
+ protect_from_forgery with: :exception
14
+
15
+ before_action :authenticate!
16
+ rescue_from RakeAudit::RecordNotFound, with: :record_not_found
17
+
18
+ private
19
+
20
+ # Run the configured authentication callable, if any, in the context of this
21
+ # controller instance. The controller is also passed as the block argument so
22
+ # procs may be written as +->(controller) { ... }+ or use +self+ directly.
23
+ #
24
+ # @return [void]
25
+ def authenticate!
26
+ callable = RakeAudit.config.authenticate_with
27
+ return unless callable
28
+
29
+ instance_exec(self, &callable)
30
+ end
31
+
32
+ # Returns the configured adapter. Falls back to a fresh ActiveRecordAdapter
33
+ # when none is set so existing apps keep working without explicit configuration.
34
+ #
35
+ # @return [RakeAudit::Adapters::Base]
36
+ def web_adapter
37
+ RakeAudit.config.adapter || default_web_adapter
38
+ end
39
+
40
+ def default_web_adapter
41
+ require 'rake_audit/adapters/active_record_adapter'
42
+ RakeAudit.config.logger.warn(
43
+ '[RakeAudit] config.adapter is nil — falling back to ActiveRecordAdapter. ' \
44
+ 'Set RakeAudit.configure { |c| c.adapter = ... } in your initializer to silence this warning.'
45
+ )
46
+ RakeAudit::Adapters::ActiveRecordAdapter.new
47
+ end
48
+
49
+ def record_not_found
50
+ head :not_found
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RakeAudit
4
+ # Renders the dashboard: six aggregate metrics plus the ten task names with
5
+ # the most failures. All data access is delegated to +web_adapter+ so the
6
+ # controller is backend-agnostic (ActiveRecord, Redis, Mongo, …).
7
+ class DashboardController < ApplicationController
8
+ # Compute the dashboard aggregates and expose them as instance variables.
9
+ # All five metrics are fetched via a single +web_adapter.stats+ call so
10
+ # adapters can compute them in one pass (e.g. one Redis LRANGE instead of five).
11
+ #
12
+ # @return [void]
13
+ def index
14
+ s = web_adapter.stats
15
+ @total = s[:total]
16
+ @success_count = s[:success_count]
17
+ @failure_count = s[:failure_count]
18
+ @failure_rate = failure_rate(@failure_count, @total)
19
+ @average_duration_ms = s[:average_duration_ms]
20
+ @top_failed_tasks = s[:top_failed_tasks]
21
+ end
22
+
23
+ private
24
+
25
+ # Percentage of executions that failed, guarding against division by zero.
26
+ #
27
+ # @param failures [Integer]
28
+ # @param total [Integer]
29
+ # @return [Float]
30
+ def failure_rate(failures, total)
31
+ return 0.0 if total.zero?
32
+
33
+ failures / total.to_f * 100
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RakeAudit
4
+ # Lists recorded task executions (with filtering + pagination) and shows the
5
+ # detail of a single execution. All data access is delegated to +web_adapter+
6
+ # so the controller is backend-agnostic (ActiveRecord, Redis, Mongo, …).
7
+ class ExecutionsController < ApplicationController
8
+ # Newest-first, filtered, paginated list of executions.
9
+ #
10
+ # @return [void]
11
+ def index
12
+ @executions = web_adapter.query(filters: filter_params, page: params[:page])
13
+ end
14
+
15
+ # Detail of one execution, looked up by id.
16
+ #
17
+ # @return [void]
18
+ def show
19
+ @execution = web_adapter.find(params[:id])
20
+ end
21
+
22
+ private
23
+
24
+ # Build a filter hash from request params, omitting any blank values so
25
+ # adapters can safely check +filters.key?(col)+ without blank-value guards.
26
+ #
27
+ # @return [Hash]
28
+ def filter_params
29
+ {
30
+ task_name: params[:task_name],
31
+ status: params[:status],
32
+ hostname: params[:hostname],
33
+ rails_env: params[:rails_env],
34
+ from: params[:from],
35
+ to: params[:to]
36
+ }.reject { |_, v| v.blank? }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RakeAudit
4
+ # ActiveRecord model backing the +rake_task_executions+ table.
5
+ #
6
+ # This is the persistence target of {RakeAudit::Adapters::ActiveRecordAdapter},
7
+ # which calls +TaskExecution.create!(record.to_h)+ with the twelve fields a
8
+ # {RakeAudit::TaskExecutionRecord} serializes. The four columns that are
9
+ # always populated for a completed execution carry presence validations so a
10
+ # malformed record fails loudly rather than persisting a partial row.
11
+ #
12
+ # When the host application defines an +ApplicationRecord+ the model inherits
13
+ # from it (picking up the app's connection and conventions); otherwise it
14
+ # falls back to +ActiveRecord::Base+ so the gem also works in a plain
15
+ # ActiveRecord setup without Rails.
16
+ class TaskExecution < (defined?(ApplicationRecord) ? ApplicationRecord : ActiveRecord::Base)
17
+ self.table_name = 'rake_task_executions'
18
+
19
+ # Treat +arguments+ as JSON on every backend so a Hash round-trips to a Hash.
20
+ # On PostgreSQL/MySQL the column is natively json(b); on SQLite it is +text+,
21
+ # where without an explicit JSON type a Hash would be stored via Ruby's
22
+ # +to_s+ and read back as a String. +ActiveRecord::Type::Json+ casts Hash to
23
+ # JSON on write and back on read for both column kinds, needs no database
24
+ # connection at load time, and never raises — unlike +serialize+, which
25
+ # rejects a natively JSON-typed column.
26
+ attribute :arguments, ActiveRecord::Type::Json.new
27
+
28
+ validates :task_name, presence: true
29
+ validates :status, presence: true
30
+ validates :started_at, presence: true
31
+ validates :finished_at, presence: true
32
+ end
33
+ end
@@ -0,0 +1,70 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>RakeAudit</title>
7
+ <style>
8
+ :root { color-scheme: light dark; }
9
+ * { box-sizing: border-box; }
10
+ body {
11
+ margin: 0;
12
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
13
+ line-height: 1.5;
14
+ color: #1f2933;
15
+ background: #f5f7fa;
16
+ }
17
+ header.rake-audit-bar {
18
+ background: #1f2933;
19
+ color: #fff;
20
+ padding: 0.75rem 1.5rem;
21
+ display: flex;
22
+ align-items: center;
23
+ gap: 1.5rem;
24
+ }
25
+ header.rake-audit-bar a { color: #cbd2d9; text-decoration: none; font-weight: 600; }
26
+ header.rake-audit-bar a:hover { color: #fff; }
27
+ header.rake-audit-bar .brand { color: #fff; font-size: 1.1rem; }
28
+ main { padding: 1.5rem; max-width: 1100px; margin: 0 auto; }
29
+ h1 { font-size: 1.4rem; margin-top: 0; }
30
+ table { width: 100%; border-collapse: collapse; background: #fff; }
31
+ th, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid #e4e7eb; }
32
+ th { background: #f0f4f8; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.03em; }
33
+ .cards { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 1.5rem; }
34
+ .card {
35
+ background: #fff;
36
+ border: 1px solid #e4e7eb;
37
+ border-radius: 8px;
38
+ padding: 1rem 1.25rem;
39
+ min-width: 150px;
40
+ flex: 1;
41
+ }
42
+ .card .label { font-size: 0.75rem; text-transform: uppercase; color: #7b8794; }
43
+ .card .value { font-size: 1.6rem; font-weight: 700; }
44
+ form.filters { background: #fff; border: 1px solid #e4e7eb; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem; }
45
+ form.filters .row { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: flex-end; }
46
+ form.filters label { display: flex; flex-direction: column; font-size: 0.75rem; color: #52606d; gap: 0.25rem; }
47
+ form.filters input, form.filters select { padding: 0.35rem 0.5rem; border: 1px solid #cbd2d9; border-radius: 4px; }
48
+ form.filters button { padding: 0.45rem 1rem; border: 0; border-radius: 4px; background: #2563eb; color: #fff; cursor: pointer; }
49
+ .status-success { color: #057a55; font-weight: 600; }
50
+ .status-failure { color: #c81e1e; font-weight: 600; }
51
+ .section { background: #fff; border: 1px solid #e4e7eb; border-radius: 8px; padding: 1rem 1.25rem; margin-bottom: 1rem; }
52
+ .section h2 { font-size: 1rem; margin-top: 0; }
53
+ dl { display: grid; grid-template-columns: max-content 1fr; gap: 0.35rem 1rem; margin: 0; }
54
+ dt { font-weight: 600; color: #52606d; }
55
+ pre { background: #f0f4f8; padding: 0.75rem; border-radius: 4px; overflow-x: auto; margin: 0; }
56
+ .pagination { margin-top: 1rem; }
57
+ .pagination a, .pagination span { padding: 0.25rem 0.5rem; }
58
+ </style>
59
+ </head>
60
+ <body>
61
+ <header class="rake-audit-bar">
62
+ <span class="brand">RakeAudit</span>
63
+ <%= link_to "Executions", executions_path %>
64
+ <%= link_to "Dashboard", dashboard_path %>
65
+ </header>
66
+ <main>
67
+ <%= yield %>
68
+ </main>
69
+ </body>
70
+ </html>
@@ -0,0 +1,48 @@
1
+ <h1>Dashboard</h1>
2
+
3
+ <section class="cards">
4
+ <div class="card">
5
+ <div class="label">Total</div>
6
+ <div class="value"><%= @total %></div>
7
+ </div>
8
+ <div class="card">
9
+ <div class="label">Success</div>
10
+ <div class="value"><%= @success_count %></div>
11
+ </div>
12
+ <div class="card">
13
+ <div class="label">Failure</div>
14
+ <div class="value"><%= @failure_count %></div>
15
+ </div>
16
+ <div class="card">
17
+ <div class="label">Failure Rate</div>
18
+ <div class="value"><%= format("%.1f%%", @failure_rate) %></div>
19
+ </div>
20
+ <div class="card">
21
+ <div class="label">Avg Duration (ms)</div>
22
+ <div class="value"><%= @average_duration_ms ? format("%.1f", @average_duration_ms) : "—" %></div>
23
+ </div>
24
+ </section>
25
+
26
+ <section class="section">
27
+ <h2>Top 10 Failed Tasks</h2>
28
+ <% if @top_failed_tasks.any? %>
29
+ <table>
30
+ <thead>
31
+ <tr>
32
+ <th>Task Name</th>
33
+ <th>Failure Count</th>
34
+ </tr>
35
+ </thead>
36
+ <tbody>
37
+ <% @top_failed_tasks.each do |task_name, failure_count| %>
38
+ <tr>
39
+ <td><%= task_name %></td>
40
+ <td><%= failure_count %></td>
41
+ </tr>
42
+ <% end %>
43
+ </tbody>
44
+ </table>
45
+ <% else %>
46
+ <p>No failed tasks recorded.</p>
47
+ <% end %>
48
+ </section>
@@ -0,0 +1,74 @@
1
+ <h1>Executions</h1>
2
+
3
+ <%= form_with url: executions_path, method: :get, class: "filters" do |f| %>
4
+ <div class="row">
5
+ <label>
6
+ Task Name
7
+ <%= f.text_field :task_name, value: params[:task_name] %>
8
+ </label>
9
+ <label>
10
+ Status
11
+ <%= f.select :status,
12
+ options_for_select(
13
+ [["All", ""], ["Success", "success"], ["Failure", "failure"]],
14
+ params[:status]
15
+ ) %>
16
+ </label>
17
+ <label>
18
+ Hostname
19
+ <%= f.text_field :hostname, value: params[:hostname] %>
20
+ </label>
21
+ <label>
22
+ Rails Env
23
+ <%= f.text_field :rails_env, value: params[:rails_env] %>
24
+ </label>
25
+ <label>
26
+ From
27
+ <%= f.date_field :from, value: params[:from] %>
28
+ </label>
29
+ <label>
30
+ To
31
+ <%= f.date_field :to, value: params[:to] %>
32
+ </label>
33
+ <button type="submit">Filter</button>
34
+ </div>
35
+ <% end %>
36
+
37
+ <table>
38
+ <thead>
39
+ <tr>
40
+ <th>Task Name</th>
41
+ <th>Status</th>
42
+ <th>Duration (ms)</th>
43
+ <th>Started At</th>
44
+ <th>Hostname</th>
45
+ <th>Rails Env</th>
46
+ <th></th>
47
+ </tr>
48
+ </thead>
49
+ <tbody>
50
+ <% if @executions.any? %>
51
+ <% @executions.each do |execution| %>
52
+ <tr>
53
+ <td><%= execution.task_name %></td>
54
+ <td class="status-<%= execution.status %>"><%= execution.status %></td>
55
+ <td><%= execution.duration_ms %></td>
56
+ <td><%= execution.started_at %></td>
57
+ <td><%= execution.hostname %></td>
58
+ <td><%= execution.rails_env %></td>
59
+ <td><%= link_to "View", execution_path(execution) %></td>
60
+ </tr>
61
+ <% end %>
62
+ <% else %>
63
+ <tr>
64
+ <td colspan="7">No executions found.</td>
65
+ </tr>
66
+ <% end %>
67
+ </tbody>
68
+ </table>
69
+
70
+ <% if @executions.respond_to?(:current_page) %>
71
+ <div class="pagination">
72
+ <%= paginate @executions if respond_to?(:paginate) %>
73
+ </div>
74
+ <% end %>