lesli_audit 1.1.0 → 1.1.2
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.
- checksums.yaml +4 -4
- data/Rakefile +6 -1
- data/app/assets/stylesheets/lesli_audit/application.css +366 -35
- data/app/controllers/lesli_audit/accounts_controller.rb +7 -3
- data/app/controllers/lesli_audit/logs_controller.rb +3 -8
- data/app/controllers/lesli_audit/visitors_controller.rb +1 -1
- data/app/interfaces/lesli_audit/logger_interface.rb +56 -59
- data/app/jobs/lesli_audit/retention_cleanup_job.rb +9 -0
- data/app/models/concerns/lesli_audit/append_only.rb +12 -0
- data/app/models/lesli_audit/account.rb +26 -10
- data/app/models/lesli_audit/account_device.rb +3 -1
- data/app/models/lesli_audit/account_log.rb +12 -7
- data/app/models/lesli_audit/account_request.rb +3 -1
- data/app/models/lesli_audit/user_journal.rb +16 -3
- data/app/models/lesli_audit/user_log.rb +12 -3
- data/app/models/lesli_audit/user_request.rb +7 -3
- data/app/services/lesli_audit/activity_audit.rb +130 -0
- data/app/services/lesli_audit/request_service.rb +2 -35
- data/app/services/lesli_audit/retention_cleanup.rb +146 -0
- data/app/services/lesli_audit/user_agent_parser.rb +51 -0
- data/app/services/lesli_audit/user_service.rb +18 -16
- data/app/services/lesli_audit/visitor_service.rb +22 -49
- data/app/views/lesli_audit/logs/show.html.erb +14 -8
- data/app/views/lesli_audit/partials/_navigation.html.erb +1 -1
- data/app/views/lesli_audit/users/index.html.erb +67 -64
- data/app/views/lesli_audit/visitors/index.html.erb +126 -80
- data/config/routes.rb +3 -2
- data/db/migrate/v1.0/0501000110_create_lesli_audit_accounts.rb +14 -1
- data/db/migrate/v1.0/0501110110_create_lesli_audit_account_logs.rb +10 -6
- data/db/migrate/v1.0/0501110210_create_lesli_audit_account_devices.rb +10 -7
- data/db/migrate/v1.0/0501110310_create_lesli_audit_account_requests.rb +10 -7
- data/db/migrate/v1.0/0501120110_create_lesli_audit_user_logs.rb +11 -7
- data/db/migrate/v1.0/0501120210_create_lesli_audit_user_journals.rb +19 -8
- data/db/migrate/v1.0/0501120310_create_lesli_audit_user_requests.rb +13 -10
- data/db/seed/devices.rb +1 -1
- data/db/seed/requests.rb +3 -3
- data/lib/lesli_audit/engine.rb +1 -6
- data/lib/lesli_audit/version.rb +2 -2
- data/lib/tasks/lesli_audit_tasks.rake +12 -4
- data/readme.md +127 -50
- metadata +14 -4
|
@@ -34,7 +34,7 @@ module LesliAudit
|
|
|
34
34
|
class VisitorService < Lesli::ApplicationLesliService
|
|
35
35
|
|
|
36
36
|
def visits
|
|
37
|
-
requests = current_user.account.audit.account_requests
|
|
37
|
+
requests = apply_time_filter(current_user.account.audit.account_requests)
|
|
38
38
|
|
|
39
39
|
controllers, total_requests = requests.pick(
|
|
40
40
|
Arel.sql("COUNT(DISTINCT request_controller)"),
|
|
@@ -47,26 +47,22 @@ module LesliAudit
|
|
|
47
47
|
}
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
def
|
|
50
|
+
def traffic
|
|
51
51
|
#Rails.cache.fetch(cache_key_for_account(__method__), expires_in: 1.hour) do
|
|
52
52
|
group = 'day'
|
|
53
53
|
#group = params[:bygroup] if ['month','week','day'].include?(params[:bygroup])
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
group_by = "DATE_TRUNC('month', created_at)" if group == 'month'
|
|
59
|
-
group_by = "DATE_TRUNC('week', created_at)" if group == 'week'
|
|
60
|
-
group_by = "DATE_TRUNC('day', created_at)" if group == 'day'
|
|
55
|
+
group_by = "DATE_TRUNC('month', date)" if group == 'month'
|
|
56
|
+
group_by = "DATE_TRUNC('week', date)" if group == 'week'
|
|
57
|
+
group_by = "DATE_TRUNC('day', date)" if group == 'day'
|
|
61
58
|
|
|
62
59
|
# compatibility for SQLite
|
|
63
60
|
if ActiveRecord::Base.connection.adapter_name == "SQLite"
|
|
64
|
-
group_by = "strftime('%Y-%m-%d',
|
|
61
|
+
group_by = "strftime('%Y-%m-%d', date)"
|
|
65
62
|
end
|
|
66
63
|
|
|
67
|
-
requests = current_user.account.audit.account_requests
|
|
68
|
-
|
|
69
|
-
requests = apply_filters(requests, query)
|
|
64
|
+
requests = apply_time_filter(current_user.account.audit.account_requests)
|
|
65
|
+
.group(group_by)
|
|
70
66
|
|
|
71
67
|
requests.limit(10).order("xaxiskey desc").select(
|
|
72
68
|
"sum(request_count) yaxiskey",
|
|
@@ -76,11 +72,9 @@ module LesliAudit
|
|
|
76
72
|
end
|
|
77
73
|
|
|
78
74
|
def requests
|
|
79
|
-
requests = current_user.account.audit.account_requests
|
|
75
|
+
requests = apply_time_filter(current_user.account.audit.account_requests)
|
|
80
76
|
.group("request_controller")
|
|
81
77
|
|
|
82
|
-
requests = apply_filters(requests, query)
|
|
83
|
-
|
|
84
78
|
requests = requests.limit(LIMIT).order("requests DESC").select(
|
|
85
79
|
:request_controller,
|
|
86
80
|
"sum(request_count) requests"
|
|
@@ -88,7 +82,7 @@ module LesliAudit
|
|
|
88
82
|
end
|
|
89
83
|
|
|
90
84
|
def os
|
|
91
|
-
device_counts = current_user.account.audit.account_devices
|
|
85
|
+
device_counts = apply_time_filter(current_user.account.audit.account_devices)
|
|
92
86
|
.group(:agent_platform)
|
|
93
87
|
.sum(:agent_count)
|
|
94
88
|
|
|
@@ -105,7 +99,7 @@ module LesliAudit
|
|
|
105
99
|
end
|
|
106
100
|
|
|
107
101
|
def devices
|
|
108
|
-
device_counts = current_user.account.audit.account_devices
|
|
102
|
+
device_counts = apply_time_filter(current_user.account.audit.account_devices)
|
|
109
103
|
.group(:agent_device)
|
|
110
104
|
.sum(:agent_count)
|
|
111
105
|
|
|
@@ -122,7 +116,7 @@ module LesliAudit
|
|
|
122
116
|
end
|
|
123
117
|
|
|
124
118
|
def browsers
|
|
125
|
-
browser_counts = current_user.account.audit.account_devices
|
|
119
|
+
browser_counts = apply_time_filter(current_user.account.audit.account_devices)
|
|
126
120
|
.group(:agent_browser)
|
|
127
121
|
.sum(:agent_count)
|
|
128
122
|
|
|
@@ -138,9 +132,9 @@ module LesliAudit
|
|
|
138
132
|
end
|
|
139
133
|
#Rails.cache.fetch(cache_key_for_account(__method__), expires_in: 4.hour) do
|
|
140
134
|
# current_user.account.audit.account_devices
|
|
141
|
-
# .group(:agent_browser, :
|
|
135
|
+
# .group(:agent_browser, :date)
|
|
142
136
|
# .select(
|
|
143
|
-
# '
|
|
137
|
+
# 'date as xaxiskey',
|
|
144
138
|
# 'agent_browser as dataname',
|
|
145
139
|
# 'sum(agent_count) as yaxiskey'
|
|
146
140
|
# ).as_json
|
|
@@ -203,37 +197,16 @@ module LesliAudit
|
|
|
203
197
|
DEVICE_ICON_CLASSES.fetch(device.to_s, DEVICE_ICON_CLASSES["unknown"])
|
|
204
198
|
end
|
|
205
199
|
|
|
206
|
-
def
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
"lower(request_agent) like :search_string or lower(request_controller) like :search_string or lower(request_action) like :search_string",
|
|
214
|
-
{
|
|
215
|
-
search_string: "%#{params[:search].downcase.gsub(" ","%")}%"
|
|
216
|
-
}
|
|
217
|
-
)
|
|
200
|
+
def apply_time_filter(relation)
|
|
201
|
+
start_date = case query[:bytime]
|
|
202
|
+
when "day" then Date.current
|
|
203
|
+
when "week" then 6.days.ago.to_date
|
|
204
|
+
when "month" then 29.days.ago.to_date
|
|
205
|
+
when "sixmonth" then 6.months.ago.to_date
|
|
206
|
+
when "year" then 1.year.ago.to_date
|
|
218
207
|
end
|
|
219
208
|
|
|
220
|
-
|
|
221
|
-
unless params[:bytime].blank? && params[:bytime] != 'all'
|
|
222
|
-
requests = requests.where(created_at: (Time.now.beginning_of_day)..) if params[:bytime] == 'day'
|
|
223
|
-
requests = requests.where(created_at: (Time.now.beginning_of_day - 7.day)..) if params[:bytime] == 'week'
|
|
224
|
-
requests = requests.where(created_at: (Time.now.beginning_of_day - 30.day)..) if params[:bytime] == 'month'
|
|
225
|
-
requests = requests.where(created_at: (Time.now.beginning_of_day - 6.month)..) if params[:bytime] == 'sixmonth'
|
|
226
|
-
requests = requests.where(created_at: (Time.now.beginning_of_day - 1.year)..) if params[:bytime] == 'year'
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
# filter by request format
|
|
230
|
-
if (!params[:byformat].blank? && ['html', 'json'].include?(params[:byformat]))
|
|
231
|
-
requests = requests.where(:request_action => 'show') if params[:byformat] == 'html'
|
|
232
|
-
requests = requests.where(:request_format => params[:byformat])
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
requests
|
|
236
|
-
|
|
209
|
+
start_date ? relation.where(date: start_date..) : relation
|
|
237
210
|
end
|
|
238
211
|
end
|
|
239
212
|
end
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
<%= render(LesliView::Layout::Container.new("audit-log")) do %>
|
|
2
|
+
<%= render(LesliView::Components::Header.new("Audit event")) %>
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
<dl class="grid grid-cols-1 gap-4 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:grid-cols-2">
|
|
5
|
+
<% %i[engine source action operation description actor_uid session_id created_at].each do |field| %>
|
|
6
|
+
<div>
|
|
7
|
+
<dt class="text-sm font-medium text-slate-500"><%= field.to_s.humanize %></dt>
|
|
8
|
+
<dd class="mt-1 break-words text-sm text-slate-900"><%= @log.public_send(field) %></dd>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
11
|
+
</dl>
|
|
4
12
|
|
|
5
|
-
<div>
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<%= button_to "Destroy this log", @log, method: :delete %>
|
|
10
|
-
</div>
|
|
13
|
+
<div class="mt-6">
|
|
14
|
+
<%= link_to "Back to logs", logs_path, class: "text-sm font-medium text-sky-700 hover:text-sky-900" %>
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
@@ -33,6 +33,6 @@ Building a better future, one line of code at a time.
|
|
|
33
33
|
%>
|
|
34
34
|
|
|
35
35
|
<%= navigation_item(lesli_audit.users_path, 'Users', 'ri-team-line'); %>
|
|
36
|
-
<%= navigation_item(lesli_audit.
|
|
36
|
+
<%= navigation_item(lesli_audit.analytics_path, 'Traffic', 'ri-line-chart-fill'); %>
|
|
37
37
|
<%= navigation_item(lesli_audit.requests_path, 'Requests', 'ri-arrow-left-right-fill'); %>
|
|
38
38
|
<%= navigation_item(lesli_audit.logs_path, 'Logs', 'ri-history-line'); %>
|
|
@@ -1,75 +1,78 @@
|
|
|
1
1
|
|
|
2
|
+
<%
|
|
3
|
+
color_classes = {
|
|
4
|
+
"info" => "text-sky-600",
|
|
5
|
+
"warning" => "text-amber-600",
|
|
6
|
+
"danger" => "text-rose-600"
|
|
7
|
+
}.freeze
|
|
8
|
+
%>
|
|
9
|
+
|
|
2
10
|
<%= render(LesliView::Layout::Container.new("audit-users")) do %>
|
|
3
11
|
<%= render(LesliView::Components::Header.new("Users")) %>
|
|
4
12
|
|
|
5
|
-
<
|
|
13
|
+
<section
|
|
14
|
+
class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5"
|
|
15
|
+
aria-label="User overview">
|
|
6
16
|
<% @users[:overview].each do |user| %>
|
|
7
|
-
<
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
</
|
|
12
|
-
</
|
|
17
|
+
<article class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
|
18
|
+
<h2 class="text-sm font-medium text-slate-500"><%= user[:label] %></h2>
|
|
19
|
+
<p class="mt-3 text-3xl font-semibold tracking-tight tabular-nums <%= color_classes.fetch(user[:color], "text-slate-700") %>">
|
|
20
|
+
<%= user[:value] %>
|
|
21
|
+
</p>
|
|
22
|
+
</article>
|
|
13
23
|
<% end %>
|
|
14
|
-
</
|
|
24
|
+
</section>
|
|
15
25
|
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
<section class="mt-6 lg:w-2/5" aria-label="Users needing attention">
|
|
27
|
+
<%= render(LesliView::Elements::Table.new(
|
|
28
|
+
columns: [],
|
|
29
|
+
headless: true,
|
|
30
|
+
striped: false,
|
|
31
|
+
compact: true,
|
|
32
|
+
class_name: "!min-w-0",
|
|
33
|
+
caption: "Users needing attention"
|
|
34
|
+
)) do |table| %>
|
|
35
|
+
<% @users[:attention_needed].each do |user| %>
|
|
36
|
+
<% table.with_row do |row| %>
|
|
37
|
+
<% row.with_cell do %>
|
|
38
|
+
<span class="inline-flex h-8 w-8 items-center justify-center <%= color_classes.fetch(user[:color], "text-slate-500") %>">
|
|
39
|
+
<span class="ri-<%= user[:icon] %> text-xl leading-none" aria-hidden="true"></span>
|
|
40
|
+
</span>
|
|
41
|
+
<% end %>
|
|
42
|
+
<% row.with_cell(class_name: "w-full font-medium text-slate-700") do %>
|
|
43
|
+
<%= user[:label] %>
|
|
44
|
+
<% end %>
|
|
45
|
+
<% row.with_cell(class_name: "font-semibold tabular-nums text-slate-900", align: :right) do %>
|
|
46
|
+
<%= user[:value] %>
|
|
36
47
|
<% end %>
|
|
37
48
|
<% end %>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
|
|
44
|
-
<div class="columns">
|
|
45
|
-
<div class="column">
|
|
46
|
-
<%= render(LesliView::Elements::Table.new(
|
|
47
|
-
columns: [{
|
|
48
|
-
label: "id",
|
|
49
|
-
field: "user_id"
|
|
50
|
-
},{
|
|
51
|
-
label: "User",
|
|
52
|
-
field: "user_email"
|
|
53
|
-
},{
|
|
54
|
-
label:"First activity",
|
|
55
|
-
field:"first_activity"
|
|
56
|
-
},{
|
|
57
|
-
label:"Last activity",
|
|
58
|
-
field:"last_activity"
|
|
59
|
-
},{
|
|
60
|
-
label:"Resources",
|
|
61
|
-
field:"resources"
|
|
62
|
-
},{
|
|
63
|
-
label:"Requests",
|
|
64
|
-
field:"requests"
|
|
65
|
-
},{
|
|
66
|
-
label:"Date",
|
|
67
|
-
field:"date"
|
|
68
|
-
}],
|
|
69
|
-
records: @working_hours
|
|
70
|
-
)) %>
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% end %>
|
|
51
|
+
</section>
|
|
73
52
|
|
|
74
|
-
<
|
|
53
|
+
<section class="mt-6 pb-8" aria-label="User working hours">
|
|
54
|
+
<%= render(LesliView::Elements::Table.new(
|
|
55
|
+
columns: [{
|
|
56
|
+
label: "id",
|
|
57
|
+
field: "user_id"
|
|
58
|
+
}, {
|
|
59
|
+
label: "User",
|
|
60
|
+
field: "user_email"
|
|
61
|
+
}, {
|
|
62
|
+
label: "First activity",
|
|
63
|
+
field: "first_activity"
|
|
64
|
+
}, {
|
|
65
|
+
label: "Last activity",
|
|
66
|
+
field: "last_activity"
|
|
67
|
+
}, {
|
|
68
|
+
label: "Requests",
|
|
69
|
+
field: "requests"
|
|
70
|
+
}, {
|
|
71
|
+
label: "Date",
|
|
72
|
+
field: "date"
|
|
73
|
+
}],
|
|
74
|
+
records: @working_hours,
|
|
75
|
+
caption: "User working hours"
|
|
76
|
+
)) %>
|
|
77
|
+
</section>
|
|
75
78
|
<% end %>
|
|
@@ -1,118 +1,164 @@
|
|
|
1
1
|
|
|
2
|
-
<%
|
|
3
|
-
|
|
2
|
+
<%
|
|
3
|
+
traffic = @traffic.reverse
|
|
4
|
+
top_days = traffic.last(3)
|
|
5
|
+
%>
|
|
4
6
|
|
|
5
7
|
<%= render(LesliView::Layout::Container.new("audit-analytics")) do %>
|
|
6
8
|
<%= render(LesliView::Components::Header.new("Analytics")) %>
|
|
7
9
|
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
</
|
|
20
|
-
|
|
10
|
+
<section
|
|
11
|
+
class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5"
|
|
12
|
+
aria-label="Traffic overview">
|
|
13
|
+
<article class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
|
14
|
+
<h2 class="text-sm font-medium text-slate-500">Controllers</h2>
|
|
15
|
+
<p class="mt-3 text-3xl font-semibold tracking-tight text-slate-900 tabular-nums">
|
|
16
|
+
<%= @visits[:controllers] %>
|
|
17
|
+
</p>
|
|
18
|
+
</article>
|
|
19
|
+
|
|
20
|
+
<article class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
|
21
|
+
<h2 class="text-sm font-medium text-slate-500">Requests</h2>
|
|
22
|
+
<p class="mt-3 text-3xl font-semibold tracking-tight text-slate-900 tabular-nums">
|
|
23
|
+
<%= @visits[:requests] %>
|
|
24
|
+
</p>
|
|
25
|
+
</article>
|
|
26
|
+
|
|
21
27
|
<% top_days.each do |day| %>
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
</
|
|
27
|
-
</
|
|
28
|
+
<article class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
|
29
|
+
<h2 class="text-sm font-medium text-slate-500"><%= day["xaxiskey"] %></h2>
|
|
30
|
+
<p class="mt-3 text-3xl font-semibold tracking-tight text-slate-900 tabular-nums">
|
|
31
|
+
<%= day["yaxiskey"] %>
|
|
32
|
+
</p>
|
|
33
|
+
</article>
|
|
28
34
|
<% end %>
|
|
29
|
-
</
|
|
35
|
+
</section>
|
|
30
36
|
|
|
31
|
-
<
|
|
37
|
+
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
|
32
38
|
<%= render(LesliView::Charts::Line.new(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
))%>
|
|
36
|
-
</
|
|
39
|
+
database_to_dataset: traffic,
|
|
40
|
+
title: "Requests"
|
|
41
|
+
)) %>
|
|
42
|
+
</section>
|
|
37
43
|
|
|
38
|
-
<
|
|
39
|
-
<
|
|
40
|
-
<%= render(LesliView::Elements::Table.new(
|
|
44
|
+
<section class="mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3" aria-label="Request traffic breakdown">
|
|
45
|
+
<article class="min-w-0">
|
|
46
|
+
<%= render(LesliView::Elements::Table.new(
|
|
47
|
+
columns: [
|
|
48
|
+
{ label: "Browsers", align: :left },
|
|
49
|
+
{ label: "Requests", align: :center }
|
|
50
|
+
],
|
|
51
|
+
class_name: "!min-w-0",
|
|
52
|
+
compact: true,
|
|
53
|
+
caption: "Requests by browser"
|
|
54
|
+
)) do |table| %>
|
|
41
55
|
<% @browsers.each do |browser| %>
|
|
42
|
-
<%
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
<% percentage = browser[:percentage].to_i.clamp(0, 100) %>
|
|
57
|
+
<% table.with_row do |row| %>
|
|
58
|
+
<% row.with_cell(class_name: "!p-0") do %>
|
|
59
|
+
<div class="relative isolate overflow-hidden px-3 py-2.5">
|
|
60
|
+
<span
|
|
61
|
+
class="absolute inset-y-0 left-0 -z-10 bg-slate-100"
|
|
62
|
+
style="width: <%= percentage %>%"
|
|
63
|
+
aria-hidden="true"></span>
|
|
64
|
+
<span class="flex items-center gap-2 text-slate-700">
|
|
65
|
+
<span class="inline-flex h-7 w-7 shrink-0 items-center justify-center text-slate-500">
|
|
66
|
+
<span class="ri-<%= browser[:icon] %> text-lg leading-none" aria-hidden="true"></span>
|
|
48
67
|
</span>
|
|
49
|
-
<span><%= browser[:browser] %></span>
|
|
68
|
+
<span class="truncate"><%= browser[:browser] %></span>
|
|
50
69
|
</span>
|
|
51
|
-
</
|
|
70
|
+
</div>
|
|
52
71
|
<% end %>
|
|
53
|
-
<% row.with_cell(
|
|
72
|
+
<% row.with_cell(class_name: "font-semibold tabular-nums text-slate-900", align: :center) do %>
|
|
54
73
|
<%= browser[:count] %>
|
|
55
74
|
<% end %>
|
|
56
75
|
<% end %>
|
|
57
76
|
<% end %>
|
|
58
77
|
<% end %>
|
|
59
|
-
</
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
</article>
|
|
79
|
+
|
|
80
|
+
<article class="min-w-0">
|
|
81
|
+
<%= render(LesliView::Elements::Table.new(
|
|
82
|
+
columns: [
|
|
83
|
+
{ label: "OS", align: :left },
|
|
84
|
+
{ label: "Requests", align: :center }
|
|
85
|
+
],
|
|
86
|
+
class_name: "!min-w-0",
|
|
87
|
+
compact: true,
|
|
88
|
+
caption: "Requests by operating system"
|
|
89
|
+
)) do |table| %>
|
|
62
90
|
<% @os.each do |device| %>
|
|
63
|
-
<%
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
91
|
+
<% percentage = device[:percentage].to_i.clamp(0, 100) %>
|
|
92
|
+
<% table.with_row do |row| %>
|
|
93
|
+
<% row.with_cell(class_name: "!p-0") do %>
|
|
94
|
+
<div class="relative isolate overflow-hidden px-3 py-2.5">
|
|
95
|
+
<span
|
|
96
|
+
class="absolute inset-y-0 left-0 -z-10 bg-slate-100"
|
|
97
|
+
style="width: <%= percentage %>%"
|
|
98
|
+
aria-hidden="true"></span>
|
|
99
|
+
<span class="flex items-center gap-2 text-slate-700">
|
|
100
|
+
<span class="inline-flex h-7 w-7 shrink-0 items-center justify-center text-slate-500">
|
|
101
|
+
<span class="ri-<%= device[:icon].downcase %> text-lg leading-none" aria-hidden="true"></span>
|
|
69
102
|
</span>
|
|
70
|
-
<span><%= device[:device] %></span>
|
|
103
|
+
<span class="truncate"><%= device[:device] %></span>
|
|
71
104
|
</span>
|
|
72
|
-
</
|
|
105
|
+
</div>
|
|
73
106
|
<% end %>
|
|
74
|
-
<% row.with_cell(
|
|
107
|
+
<% row.with_cell(class_name: "font-semibold tabular-nums text-slate-900", align: :center) do %>
|
|
75
108
|
<%= device[:count] %>
|
|
76
109
|
<% end %>
|
|
77
110
|
<% end %>
|
|
78
111
|
<% end %>
|
|
79
112
|
<% end %>
|
|
80
|
-
</
|
|
81
|
-
|
|
82
|
-
|
|
113
|
+
</article>
|
|
114
|
+
|
|
115
|
+
<article class="min-w-0">
|
|
116
|
+
<%= render(LesliView::Elements::Table.new(
|
|
117
|
+
columns: [
|
|
118
|
+
{ label: "Device", align: :left },
|
|
119
|
+
{ label: "Requests", align: :center }
|
|
120
|
+
],
|
|
121
|
+
class_name: "!min-w-0",
|
|
122
|
+
compact: true,
|
|
123
|
+
caption: "Requests by device"
|
|
124
|
+
)) do |table| %>
|
|
83
125
|
<% @devices.each do |device| %>
|
|
84
|
-
<%
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
126
|
+
<% percentage = device[:percentage].to_i.clamp(0, 100) %>
|
|
127
|
+
<% table.with_row do |row| %>
|
|
128
|
+
<% row.with_cell(class_name: "!p-0") do %>
|
|
129
|
+
<div class="relative isolate overflow-hidden px-3 py-2.5">
|
|
130
|
+
<span
|
|
131
|
+
class="absolute inset-y-0 left-0 -z-10 bg-slate-100"
|
|
132
|
+
style="width: <%= percentage %>%"
|
|
133
|
+
aria-hidden="true"></span>
|
|
134
|
+
<span class="flex items-center gap-2 text-slate-700">
|
|
135
|
+
<span class="inline-flex h-7 w-7 shrink-0 items-center justify-center text-slate-500">
|
|
136
|
+
<span class="ri-<%= device[:icon] %> text-lg leading-none" aria-hidden="true"></span>
|
|
90
137
|
</span>
|
|
91
|
-
<span><%= device[:device] %></span>
|
|
138
|
+
<span class="truncate"><%= device[:device] %></span>
|
|
92
139
|
</span>
|
|
93
|
-
</
|
|
140
|
+
</div>
|
|
94
141
|
<% end %>
|
|
95
|
-
<% row.with_cell(
|
|
142
|
+
<% row.with_cell(class_name: "font-semibold tabular-nums text-slate-900", align: :center) do %>
|
|
96
143
|
<%= device[:count] %>
|
|
97
144
|
<% end %>
|
|
98
145
|
<% end %>
|
|
99
146
|
<% end %>
|
|
100
147
|
<% end %>
|
|
101
|
-
</
|
|
102
|
-
</
|
|
103
|
-
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
</div>
|
|
148
|
+
</article>
|
|
149
|
+
</section>
|
|
150
|
+
|
|
151
|
+
<section class="mt-6 pb-8" aria-label="Top controllers">
|
|
152
|
+
<%= render(LesliView::Elements::Table.new(
|
|
153
|
+
columns: [{
|
|
154
|
+
label: "Top controllers",
|
|
155
|
+
field: "request_controller"
|
|
156
|
+
}, {
|
|
157
|
+
label: "Requests",
|
|
158
|
+
field: "requests"
|
|
159
|
+
}],
|
|
160
|
+
records: @requests,
|
|
161
|
+
caption: "Requests by controller"
|
|
162
|
+
)) %>
|
|
163
|
+
</section>
|
|
118
164
|
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -43,11 +43,12 @@ LesliAudit::Engine.routes.draw do
|
|
|
43
43
|
resources :users, only: [:index]
|
|
44
44
|
|
|
45
45
|
# Analytics:
|
|
46
|
-
#
|
|
46
|
+
# Requests by date
|
|
47
47
|
# Trends by date
|
|
48
48
|
# Most active users
|
|
49
49
|
# Most active controllers
|
|
50
|
-
|
|
50
|
+
get "analytics", to: "visitors#index", as: :analytics
|
|
51
|
+
resources :visitors, only: [:index]
|
|
51
52
|
|
|
52
53
|
resources :logs, only: [:index, :show]
|
|
53
54
|
|
|
@@ -30,8 +30,21 @@ Building a better future, one line of code at a time.
|
|
|
30
30
|
// ·
|
|
31
31
|
=end
|
|
32
32
|
|
|
33
|
-
class CreateLesliAuditAccounts < ActiveRecord::Migration[
|
|
33
|
+
class CreateLesliAuditAccounts < ActiveRecord::Migration[8.1]
|
|
34
34
|
def change
|
|
35
35
|
create_table_lesli_shared_account_10(:lesli_audit)
|
|
36
|
+
|
|
37
|
+
remove_index :lesli_audit_accounts, :account_id
|
|
38
|
+
add_index :lesli_audit_accounts, :account_id,
|
|
39
|
+
unique: true,
|
|
40
|
+
name: "index_lesli_audit_accounts_on_account_id"
|
|
41
|
+
|
|
42
|
+
# Soft deletion keeps the core row and its audit history. Only a
|
|
43
|
+
# deliberate hard deletion activates this cascade.
|
|
44
|
+
remove_foreign_key :lesli_audit_accounts, :lesli_accounts,
|
|
45
|
+
column: :account_id
|
|
46
|
+
add_foreign_key :lesli_audit_accounts, :lesli_accounts,
|
|
47
|
+
column: :account_id,
|
|
48
|
+
on_delete: :cascade
|
|
36
49
|
end
|
|
37
50
|
end
|
|
@@ -30,24 +30,28 @@ Building a better future, one line of code at a time.
|
|
|
30
30
|
// ·
|
|
31
31
|
=end
|
|
32
32
|
|
|
33
|
-
class CreateLesliAuditAccountLogs < ActiveRecord::Migration[
|
|
33
|
+
class CreateLesliAuditAccountLogs < ActiveRecord::Migration[8.1]
|
|
34
34
|
def change
|
|
35
35
|
create_table :lesli_audit_account_logs do |t|
|
|
36
36
|
t.string :engine
|
|
37
|
+
t.string :source
|
|
37
38
|
t.string :action
|
|
38
39
|
|
|
39
40
|
# Polymorphic subject
|
|
40
41
|
t.string :subject_type
|
|
41
42
|
t.bigint :subject_id
|
|
42
43
|
|
|
43
|
-
t.string :operation
|
|
44
|
-
t.
|
|
44
|
+
t.string :operation, null: false
|
|
45
|
+
t.text :description
|
|
45
46
|
t.string :session_id
|
|
47
|
+
t.string :actor_uid
|
|
46
48
|
|
|
47
|
-
t.timestamps
|
|
49
|
+
t.timestamps null: false
|
|
48
50
|
end
|
|
49
51
|
|
|
50
|
-
add_reference(:lesli_audit_account_logs, :user, foreign_key: { to_table: :lesli_users }, null: true)
|
|
51
|
-
add_reference(:lesli_audit_account_logs, :account, foreign_key: { to_table: :lesli_audit_accounts }, null:
|
|
52
|
+
add_reference(:lesli_audit_account_logs, :user, foreign_key: { to_table: :lesli_users, on_delete: :nullify }, null: true)
|
|
53
|
+
add_reference(:lesli_audit_account_logs, :account, foreign_key: { to_table: :lesli_audit_accounts, on_delete: :cascade }, null: false)
|
|
54
|
+
add_index(:lesli_audit_account_logs, %i[account_id created_at], name: "index_lesli_audit_account_logs_on_account_and_created_at")
|
|
55
|
+
add_index(:lesli_audit_account_logs, :created_at, name: "index_lesli_audit_account_logs_on_created_at")
|
|
52
56
|
end
|
|
53
57
|
end
|