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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +6 -1
  3. data/app/assets/stylesheets/lesli_audit/application.css +366 -35
  4. data/app/controllers/lesli_audit/accounts_controller.rb +7 -3
  5. data/app/controllers/lesli_audit/logs_controller.rb +3 -8
  6. data/app/controllers/lesli_audit/visitors_controller.rb +1 -1
  7. data/app/interfaces/lesli_audit/logger_interface.rb +56 -59
  8. data/app/jobs/lesli_audit/retention_cleanup_job.rb +9 -0
  9. data/app/models/concerns/lesli_audit/append_only.rb +12 -0
  10. data/app/models/lesli_audit/account.rb +26 -10
  11. data/app/models/lesli_audit/account_device.rb +3 -1
  12. data/app/models/lesli_audit/account_log.rb +12 -7
  13. data/app/models/lesli_audit/account_request.rb +3 -1
  14. data/app/models/lesli_audit/user_journal.rb +16 -3
  15. data/app/models/lesli_audit/user_log.rb +12 -3
  16. data/app/models/lesli_audit/user_request.rb +7 -3
  17. data/app/services/lesli_audit/activity_audit.rb +130 -0
  18. data/app/services/lesli_audit/request_service.rb +2 -35
  19. data/app/services/lesli_audit/retention_cleanup.rb +146 -0
  20. data/app/services/lesli_audit/user_agent_parser.rb +51 -0
  21. data/app/services/lesli_audit/user_service.rb +18 -16
  22. data/app/services/lesli_audit/visitor_service.rb +22 -49
  23. data/app/views/lesli_audit/logs/show.html.erb +14 -8
  24. data/app/views/lesli_audit/partials/_navigation.html.erb +1 -1
  25. data/app/views/lesli_audit/users/index.html.erb +67 -64
  26. data/app/views/lesli_audit/visitors/index.html.erb +126 -80
  27. data/config/routes.rb +3 -2
  28. data/db/migrate/v1.0/0501000110_create_lesli_audit_accounts.rb +14 -1
  29. data/db/migrate/v1.0/0501110110_create_lesli_audit_account_logs.rb +10 -6
  30. data/db/migrate/v1.0/0501110210_create_lesli_audit_account_devices.rb +10 -7
  31. data/db/migrate/v1.0/0501110310_create_lesli_audit_account_requests.rb +10 -7
  32. data/db/migrate/v1.0/0501120110_create_lesli_audit_user_logs.rb +11 -7
  33. data/db/migrate/v1.0/0501120210_create_lesli_audit_user_journals.rb +19 -8
  34. data/db/migrate/v1.0/0501120310_create_lesli_audit_user_requests.rb +13 -10
  35. data/db/seed/devices.rb +1 -1
  36. data/db/seed/requests.rb +3 -3
  37. data/lib/lesli_audit/engine.rb +1 -6
  38. data/lib/lesli_audit/version.rb +2 -2
  39. data/lib/tasks/lesli_audit_tasks.rake +12 -4
  40. data/readme.md +127 -50
  41. 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 visitors
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
- # only the users of the account
56
- usrs = current_user.account.users
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', created_at)"
61
+ group_by = "strftime('%Y-%m-%d', date)"
65
62
  end
66
63
 
67
- requests = current_user.account.audit.account_requests.group(group_by)
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, :created_at)
135
+ # .group(:agent_browser, :date)
142
136
  # .select(
143
- # 'created_at as xaxiskey',
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 apply_filters requests, params
207
-
208
- return requests
209
-
210
- # filter by search string
211
- unless params[:search].blank?
212
- requests = requests.where(
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
- # filter by time
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
- <p style="color: green"><%= notice %></p>
1
+ <%= render(LesliView::Layout::Container.new("audit-log")) do %>
2
+ <%= render(LesliView::Components::Header.new("Audit event")) %>
2
3
 
3
- <%= render @log %>
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
- <%= link_to "Edit this log", edit_log_path(@log) %> |
7
- <%= link_to "Back to logs", logs_path %>
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.visitors_path, 'Visitors', 'ri-line-chart-fill'); %>
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
- <div class="columns">
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
- <div class="column">
8
- <div class="box">
9
- <h4><%= user[:label] %></h4>
10
- <p class="is-size-3 has-text-<%= user[:color] %>"><%= user[:value] %></p>
11
- </div>
12
- </div>
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
- </div>
24
+ </section>
15
25
 
16
- <div class="columns">
17
- <div class="column">
18
- <div class="box">
19
- <%= render(LesliView::Elements::Table.new(columns: [])) do |table| %>
20
- <% @users[:attention_needed].each do |user| %>
21
- <% table.with_row(css_class:"is-white") do |row| %>
22
- <% row.with_cell(css_class:"") do %>
23
- <span class="icon has-text-<%= user[:color] %>">
24
- <span class="is-size-4 ri-<%= user[:icon] %>"></span>
25
- </span>
26
- <% end %>
27
- <% row.with_cell(css_class:"is-fullwidth") do %>
28
- <%= user[:label] %>
29
- <% end %>
30
- <% row.with_cell(css_class:"has-text-right") do %>
31
- <span class="is-size-6 has-text-weight-bold">
32
- <%= user[:value] %>
33
- </span>
34
- <% end %>
35
- <% end %>
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
- </div>
39
- </div>
40
- <div class="column is-three-fifths">
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
- <br>
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
- <% @visitors = @visitors.reverse %>
3
- <% top_days = (@visitors.last(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
- <div class="columns">
9
- <div class="column">
10
- <div class="box">
11
- <h4>Controllers</h4>
12
- <p class="is-size-3"><%= @visits[:controllers] %></p>
13
- </div>
14
- </div>
15
- <div class="column">
16
- <div class="box">
17
- <h4>Requests</h4>
18
- <p class="is-size-3"><%= @visits[:requests] %></p>
19
- </div>
20
- </div>
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
- <div class="column">
23
- <div class="box">
24
- <h4><%= day['xaxiskey'] %></h4>
25
- <p class="is-size-3"><%= day['yaxiskey'] %></p>
26
- </div>
27
- </div>
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
- </div>
35
+ </section>
30
36
 
31
- <div class="box">
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
- title:"Visitors",
34
- database_to_dataset: @visitors
35
- ))%>
36
- </div>
39
+ database_to_dataset: traffic,
40
+ title: "Requests"
41
+ )) %>
42
+ </section>
37
43
 
38
- <div class="columns">
39
- <div class="column">
40
- <%= render(LesliView::Elements::Table.new(columns: [{label:'Browsers', align:'left'},{label:'Visitors', align:'center'}])) do |table| %>
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
- <% table.with_row(css_class:"") do |row| %>
43
- <% row.with_cell(css_class:"p-0") do %>
44
- <p style="padding:8px;background: linear-gradient(to right, #E5EAF0 <%= browser[:percentage] %>%, transparent 0%);">
45
- <span class="icon-text">
46
- <span class="icon">
47
- <span class="ri-<%= browser[:icon] %>"></span>
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
- </p>
70
+ </div>
52
71
  <% end %>
53
- <% row.with_cell(css_class:"has-text-centered") do %>
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
- </div>
60
- <div class="column">
61
- <%= render(LesliView::Elements::Table.new(columns: [{label:'OS', align:'left'},{label:'Visitors', align:'center'}])) do |table| %>
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
- <% table.with_row(css_class:"") do |row| %>
64
- <% row.with_cell(css_class:"p-0") do %>
65
- <p style="padding:8px;background: linear-gradient(to right, #E5EAF0 <%= device[:percentage] %>%, transparent 0%);">
66
- <span class="icon-text">
67
- <span class="icon">
68
- <span class="ri-<%= device[:icon].downcase %>"></span>
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
- </p>
105
+ </div>
73
106
  <% end %>
74
- <% row.with_cell(css_class:"has-text-centered") do %>
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
- </div>
81
- <div class="column">
82
- <%= render(LesliView::Elements::Table.new(columns: [{label:'Device', align:'left'},{label:'Visitors', align:'center'}])) do |table| %>
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
- <% table.with_row(css_class:"") do |row| %>
85
- <% row.with_cell(css_class:"p-0") do %>
86
- <p style="padding:8px;background: linear-gradient(to right, #E5EAF0 <%= device[:percentage] %>%, transparent 0%);">
87
- <span class="icon-text">
88
- <span class="icon">
89
- <span class="ri-<%= device[:icon] %>"></span>
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
- </p>
140
+ </div>
94
141
  <% end %>
95
- <% row.with_cell(css_class:"has-text-centered") do %>
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
- </div>
102
- </div>
103
-
104
- <div class="columns">
105
- <div class="column">
106
- <%= render(LesliView::Elements::Table.new(
107
- columns: [{
108
- label: "Top controllers",
109
- field: "request_controller"
110
- },{
111
- label:"Requests",
112
- field:"requests"
113
- }],
114
- records: @requests
115
- )) %>
116
- </div>
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
- # Visitors by date
46
+ # Requests by date
47
47
  # Trends by date
48
48
  # Most active users
49
49
  # Most active controllers
50
- resources :visitors, only: [:index]
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[6.0]
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[6.0]
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.string :description
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: true)
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