lesli_audit 1.1.0 → 1.1.1
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/app/assets/stylesheets/lesli_audit/application.tailwind.css +2 -0
- data/app/models/concerns/lesli_audit/user_associations.rb +11 -0
- data/app/views/lesli_audit/users/index.html.erb +70 -64
- data/app/views/lesli_audit/visitors/index.html.erb +126 -80
- data/lib/lesli_audit/engine.rb +6 -0
- data/lib/lesli_audit/version.rb +2 -2
- data/readme.md +113 -49
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ebf3748b965f8b36dcd0861b281f5fabf373d59e71661651b01cece8bdc4c18
|
|
4
|
+
data.tar.gz: 712eefe307ebef94bbc2b03f94ecfa608303d30d58688c99ed303fa248b7b8d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 372f1ff92fd88a88d80ae01a94fe8d2f5f5c754922cc7bbe596f6207cac738fe7799762b9393457c2b6806c5dafd0fb1d0fa04bc9e88ce2eddb6c804d25a19a8
|
|
7
|
+
data.tar.gz: ab4e0943748344c2fedeb6b32f70e241f710a2ec28f95b730d986a0a486da83ca95ac2f6d81aa9fabd18ba8f41f6f9cd3cf658696a6d418df58e293f95344958
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--color-amber-600:oklch(66.6% .179 58.318);--color-sky-600:oklch(58.8% .158 241.966);--color-rose-600:oklch(58.6% .253 17.585);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-900:oklch(20.8% .042 265.755);--color-white:#fff;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--radius-2xl:1rem}}@layer utilities{.absolute{position:absolute}.relative{position:relative}.inset-y-0{inset-block:0}.left-0{left:0}.isolate{isolation:isolate}.-z-10{z-index:calc(10 * -1)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-6{margin-top:calc(var(--spacing) * 6)}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-full{width:100%}.\!min-w-0{min-width:0!important}.min-w-0{min-width:0}.shrink-0{flex-shrink:0}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.rounded-2xl{border-radius:var(--radius-2xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-slate-200{border-color:var(--color-slate-200)}.bg-slate-100{background-color:var(--color-slate-100)}.bg-white{background-color:var(--color-white)}.\!p-0{padding:0!important}.p-5{padding:calc(var(--spacing) * 5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.pb-8{padding-bottom:calc(var(--spacing) * 8)}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.leading-none{--tw-leading:1;line-height:1}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.text-amber-600{color:var(--color-amber-600)}.text-rose-600{color:var(--color-rose-600)}.text-sky-600{color:var(--color-sky-600)}.text-slate-500{color:var(--color-slate-500)}.text-slate-700{color:var(--color-slate-700)}.text-slate-900{color:var(--color-slate-900)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}@media (min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:64rem){.lg\:w-2\/5{width:40%}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module LesliAudit
|
|
2
|
+
module UserAssociations
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
has_many :requests, class_name: "LesliAudit::UserRequest"
|
|
7
|
+
has_many :journals, class_name: "LesliAudit::UserJournal"
|
|
8
|
+
has_many :logs, class_name: "LesliAudit::UserLog"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -1,75 +1,81 @@
|
|
|
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: "Resources",
|
|
69
|
+
field: "resources"
|
|
70
|
+
}, {
|
|
71
|
+
label: "Requests",
|
|
72
|
+
field: "requests"
|
|
73
|
+
}, {
|
|
74
|
+
label: "Date",
|
|
75
|
+
field: "date"
|
|
76
|
+
}],
|
|
77
|
+
records: @working_hours,
|
|
78
|
+
caption: "User working hours"
|
|
79
|
+
)) %>
|
|
80
|
+
</section>
|
|
75
81
|
<% end %>
|
|
@@ -1,118 +1,164 @@
|
|
|
1
1
|
|
|
2
|
-
<%
|
|
3
|
-
|
|
2
|
+
<%
|
|
3
|
+
visitors = @visitors.reverse
|
|
4
|
+
top_days = visitors.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="Visitor 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
|
-
title:"Visitors",
|
|
34
|
-
database_to_dataset:
|
|
35
|
-
))%>
|
|
36
|
-
</
|
|
39
|
+
title: "Visitors",
|
|
40
|
+
database_to_dataset: visitors
|
|
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="Visitor breakdown">
|
|
45
|
+
<article class="min-w-0">
|
|
46
|
+
<%= render(LesliView::Elements::Table.new(
|
|
47
|
+
columns: [
|
|
48
|
+
{ label: "Browsers", align: :left },
|
|
49
|
+
{ label: "Visitors", align: :center }
|
|
50
|
+
],
|
|
51
|
+
class_name: "!min-w-0",
|
|
52
|
+
compact: true,
|
|
53
|
+
caption: "Visitors 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: "Visitors", align: :center }
|
|
85
|
+
],
|
|
86
|
+
class_name: "!min-w-0",
|
|
87
|
+
compact: true,
|
|
88
|
+
caption: "Visitors 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: "Visitors", align: :center }
|
|
120
|
+
],
|
|
121
|
+
class_name: "!min-w-0",
|
|
122
|
+
compact: true,
|
|
123
|
+
caption: "Visitors 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/lib/lesli_audit/engine.rb
CHANGED
|
@@ -3,6 +3,12 @@ module LesliAudit
|
|
|
3
3
|
class Engine < ::Rails::Engine
|
|
4
4
|
isolate_namespace LesliAudit
|
|
5
5
|
|
|
6
|
+
config.to_prepare do
|
|
7
|
+
unless Lesli::User < LesliAudit::UserAssociations
|
|
8
|
+
Lesli::User.include(LesliAudit::UserAssociations)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
6
12
|
initializer :lesli_admin do |app|
|
|
7
13
|
|
|
8
14
|
# register assets manifest
|
data/lib/lesli_audit/version.rb
CHANGED
data/readme.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<
|
|
3
|
-
|
|
2
|
+
<h1 align="center">
|
|
3
|
+
<img width="100" alt="LesliAudit" src="./app/assets/images/lesli_audit/audit-logo.svg" />
|
|
4
|
+
</h1>
|
|
5
|
+
<h3 align="center">Audit trails and analytics for the Lesli Framework.</h3>
|
|
4
6
|
</div>
|
|
5
7
|
|
|
6
8
|
<br />
|
|
7
|
-
<hr/>
|
|
8
9
|
|
|
9
10
|
<div align="center">
|
|
10
|
-
<a target="_blank" href="https://github.com/LesliTech/LesliAudit/actions">
|
|
11
|
-
<img alt="
|
|
11
|
+
<a target="_blank" href="https://github.com/LesliTech/LesliAudit/actions/workflows/lesli-ci-tests.yaml">
|
|
12
|
+
<img alt="LesliAudit test status" src="https://img.shields.io/github/actions/workflow/status/LesliTech/LesliAudit/lesli-ci-tests.yaml?branch=master&style=for-the-badge&logo=github&label=tests">
|
|
12
13
|
</a>
|
|
13
14
|
<a target="_blank" href="https://rubygems.org/gems/lesli_audit">
|
|
14
15
|
<img alt="Gem Version" src="https://img.shields.io/gem/v/lesli_audit?style=for-the-badge&logo=ruby">
|
|
@@ -16,83 +17,149 @@
|
|
|
16
17
|
<a target="_blank" href="https://codecov.io/github/LesliTech/LesliAudit">
|
|
17
18
|
<img alt="Codecov" src="https://img.shields.io/codecov/c/github/LesliTech/LesliAudit?style=for-the-badge&logo=codecov">
|
|
18
19
|
</a>
|
|
20
|
+
<a target="_blank" href="https://sonarcloud.io/project/overview?id=LesliTech_LesliAudit">
|
|
21
|
+
<img alt="Sonar Quality Gate" src="https://img.shields.io/sonar/quality_gate/LesliTech_LesliAudit?server=https%3A%2F%2Fsonarcloud.io&style=for-the-badge&logo=sonarqubecloud&label=Quality">
|
|
22
|
+
</a>
|
|
19
23
|
</div>
|
|
20
24
|
|
|
21
|
-
<hr/>
|
|
22
25
|
<br />
|
|
23
26
|
|
|
24
27
|
<div align="center">
|
|
25
28
|
<img
|
|
26
29
|
style="width:100%;max-width:800px;border-radius:6px;"
|
|
27
|
-
alt="
|
|
30
|
+
alt="LesliAudit activity and request history"
|
|
31
|
+
src="./docs/images/screenshot.png" />
|
|
28
32
|
</div>
|
|
29
33
|
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
<br />
|
|
37
|
+
|
|
38
|
+
## Introduction
|
|
39
|
+
|
|
40
|
+
LesliAudit is the official audit trail and analytics engine for the [Lesli Framework](https://github.com/LesliTech/Lesli).
|
|
41
|
+
|
|
42
|
+
It records account activity and provides administrators with searchable operational history and usage insights.
|
|
43
|
+
|
|
30
44
|
<br />
|
|
31
|
-
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
- Account activity and audit logs
|
|
49
|
+
- Request and navigation history
|
|
50
|
+
- Visitor and authenticated-user analytics
|
|
51
|
+
- Device and user-agent information
|
|
52
|
+
- Detailed views for investigating recorded activity
|
|
53
|
+
|
|
54
|
+
<br />
|
|
55
|
+
|
|
56
|
+
## Try LesliAudit
|
|
57
|
+
|
|
58
|
+
- [Try the online demo](https://demo.lesli.dev/)
|
|
59
|
+
- [Run the Docker demo](https://github.com/LesliTech/lesli-docker-demo)
|
|
60
|
+
|
|
32
61
|
<br />
|
|
33
62
|
|
|
34
|
-
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
### Requirements
|
|
66
|
+
|
|
67
|
+
- A Rails application with [Lesli](https://rubygems.org/gems/lesli)
|
|
68
|
+
- SQLite by default, or PostgreSQL when preferred by the host application
|
|
69
|
+
|
|
70
|
+
### Install LesliAudit
|
|
71
|
+
|
|
72
|
+
Add the engine to the host Rails application and prepare its database:
|
|
35
73
|
|
|
36
74
|
```shell
|
|
37
|
-
# Add LesliAudit engine gem
|
|
38
75
|
bundle add lesli_audit
|
|
76
|
+
bin/rails lesli:db:prepare
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Mount the engine
|
|
39
80
|
|
|
40
|
-
|
|
41
|
-
|
|
81
|
+
Applications using Lesli's standard router mount LesliAudit automatically at `/audit`:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# config/routes.rb
|
|
85
|
+
Rails.application.routes.draw do
|
|
86
|
+
Lesli::Router.mount(self)
|
|
87
|
+
end
|
|
42
88
|
```
|
|
43
89
|
|
|
90
|
+
If the application does not use the standard Lesli router, mount the engine directly:
|
|
91
|
+
|
|
44
92
|
```ruby
|
|
45
|
-
#
|
|
93
|
+
# config/routes.rb
|
|
46
94
|
Rails.application.routes.draw do
|
|
47
95
|
mount LesliAudit::Engine => "/audit"
|
|
48
96
|
end
|
|
49
97
|
```
|
|
50
98
|
|
|
51
|
-
|
|
52
|
-
|
|
99
|
+
Start Rails and visit [http://127.0.0.1:3000/audit](http://127.0.0.1:3000/audit):
|
|
100
|
+
|
|
101
|
+
```shell
|
|
102
|
+
bin/rails server
|
|
103
|
+
```
|
|
104
|
+
|
|
53
105
|
<br />
|
|
54
106
|
|
|
55
|
-
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
Clone LesliAudit into the host application's `engines` directory:
|
|
56
110
|
|
|
57
111
|
```shell
|
|
58
|
-
|
|
59
|
-
|
|
112
|
+
cd RailsApp
|
|
113
|
+
mkdir -p engines
|
|
114
|
+
git clone https://github.com/LesliTech/LesliAudit.git engines/LesliAudit
|
|
115
|
+
```
|
|
60
116
|
|
|
61
|
-
|
|
62
|
-
gem "lesli", path: "engines/LesliAudit"
|
|
117
|
+
Reference the local engine from the host application's `Gemfile`:
|
|
63
118
|
|
|
64
|
-
|
|
119
|
+
```ruby
|
|
120
|
+
gem "lesli_audit", path: "engines/LesliAudit"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Install dependencies, prepare the host database, and start Rails:
|
|
124
|
+
|
|
125
|
+
```shell
|
|
65
126
|
bundle install
|
|
127
|
+
bin/rails lesli:db:prepare
|
|
128
|
+
bin/rails server
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Tests
|
|
66
132
|
|
|
67
|
-
|
|
68
|
-
rake lesli:db:setup
|
|
133
|
+
From a complete Lesli development workspace, run the engine test suite from the LesliAudit directory:
|
|
69
134
|
|
|
135
|
+
```shell
|
|
136
|
+
cd engines/LesliAudit
|
|
137
|
+
bin/rails test
|
|
70
138
|
```
|
|
71
139
|
|
|
72
140
|
<br />
|
|
73
|
-
<hr/>
|
|
74
|
-
<br />
|
|
75
|
-
|
|
76
|
-
### Demo
|
|
77
141
|
|
|
78
|
-
|
|
79
|
-
* [docker demo](https://github.com/LesliTech/lesli-docker-demo)
|
|
142
|
+
## Documentation
|
|
80
143
|
|
|
144
|
+
- [Lesli website](https://www.lesli.dev/)
|
|
145
|
+
- [Documentation](https://www.lesli.dev/engines/audit)
|
|
146
|
+
- [Frontend development](./docs/about/frontend.md)
|
|
147
|
+
- [Release notes](https://github.com/LesliTech/LesliAudit/releases)
|
|
148
|
+
- [Issue tracker](https://github.com/LesliTech/LesliAudit/issues)
|
|
149
|
+
- [Source code](https://github.com/LesliTech/LesliAudit)
|
|
81
150
|
|
|
82
|
-
|
|
83
|
-
* [website](https://www.lesli.dev/)
|
|
84
|
-
* [documentation](https://www.lesli.dev/engines/audit)
|
|
151
|
+
<br />
|
|
85
152
|
|
|
153
|
+
## Community
|
|
86
154
|
|
|
87
|
-
|
|
155
|
+
- [X: @LesliTech](https://x.com/LesliTech)
|
|
156
|
+
- [hello@lesli.tech](mailto:hello@lesli.tech)
|
|
157
|
+
- [https://www.lesli.tech](https://www.lesli.tech)
|
|
88
158
|
|
|
89
|
-
|
|
90
|
-
* [Email: hello@lesli.tech](hello@lesli.tech)
|
|
91
|
-
* [Website: https://www.lesli.tech](https://www.lesli.tech)
|
|
159
|
+
<br />
|
|
92
160
|
|
|
161
|
+
## License
|
|
93
162
|
|
|
94
|
-
### License
|
|
95
|
-
-------
|
|
96
163
|
Copyright (c) 2026, Lesli Technologies, S. A.
|
|
97
164
|
|
|
98
165
|
This program is free software: you can redistribute it and/or modify
|
|
@@ -106,19 +173,16 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
106
173
|
GNU General Public License for more details.
|
|
107
174
|
|
|
108
175
|
You should have received a copy of the GNU General Public License
|
|
109
|
-
along with this program. If not, see
|
|
176
|
+
along with this program. If not, see [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/).
|
|
110
177
|
|
|
111
|
-
|
|
112
|
-
<hr />
|
|
113
|
-
<br />
|
|
114
|
-
<br />
|
|
178
|
+
The complete license text is available in the [license file](./license).
|
|
115
179
|
|
|
116
|
-
|
|
117
|
-
<img width="200" alt="Lesli logo" src="https://cdn.lesli.tech/lesli/brand/app-logo.svg" />
|
|
118
|
-
<h3 align="center" class="mt-0">
|
|
119
|
-
The Open-Source SaaS Development Framework for Ruby on Rails.
|
|
120
|
-
</h3>
|
|
121
|
-
</div>
|
|
180
|
+
---
|
|
122
181
|
|
|
123
182
|
<br />
|
|
124
183
|
<br />
|
|
184
|
+
|
|
185
|
+
<div align="center">
|
|
186
|
+
<img width="80" alt="Lesli icon" src="https://cdn.lesli.tech/lesli/brand/app-icon.svg" />
|
|
187
|
+
<h3 align="center">The Open-Source SaaS Development Framework for Ruby on Rails.</h3>
|
|
188
|
+
</div>
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lesli_audit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- The Lesli Development Team
|
|
@@ -48,6 +48,7 @@ files:
|
|
|
48
48
|
- app/assets/images/lesli_audit/audit-logo.svg
|
|
49
49
|
- app/assets/javascripts/lesli_audit/application.js
|
|
50
50
|
- app/assets/stylesheets/lesli_audit/application.css
|
|
51
|
+
- app/assets/stylesheets/lesli_audit/application.tailwind.css
|
|
51
52
|
- app/controllers/lesli_audit/accounts_controller.rb
|
|
52
53
|
- app/controllers/lesli_audit/application_controller.rb
|
|
53
54
|
- app/controllers/lesli_audit/dashboards_controller.rb
|
|
@@ -65,6 +66,7 @@ files:
|
|
|
65
66
|
- app/interfaces/lesli_audit/logger_interface.rb
|
|
66
67
|
- app/jobs/lesli_audit/application_job.rb
|
|
67
68
|
- app/mailers/lesli_audit/application_mailer.rb
|
|
69
|
+
- app/models/concerns/lesli_audit/user_associations.rb
|
|
68
70
|
- app/models/lesli_audit/account.rb
|
|
69
71
|
- app/models/lesli_audit/account_device.rb
|
|
70
72
|
- app/models/lesli_audit/account_log.rb
|