sinaliza 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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +186 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/sinaliza/application.css +173 -0
- data/app/controllers/concerns/sinaliza/traceable.rb +47 -0
- data/app/controllers/sinaliza/application_controller.rb +4 -0
- data/app/controllers/sinaliza/events_controller.rb +41 -0
- data/app/helpers/sinaliza/application_helper.rb +4 -0
- data/app/jobs/sinaliza/application_job.rb +4 -0
- data/app/jobs/sinaliza/record_event_job.rb +19 -0
- data/app/mailers/sinaliza/application_mailer.rb +6 -0
- data/app/models/concerns/sinaliza/trackable.rb +39 -0
- data/app/models/sinaliza/application_record.rb +5 -0
- data/app/models/sinaliza/event.rb +32 -0
- data/app/views/layouts/sinaliza/application.html.erb +17 -0
- data/app/views/sinaliza/events/_filters.html.erb +53 -0
- data/app/views/sinaliza/events/index.html.erb +45 -0
- data/app/views/sinaliza/events/show.html.erb +79 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20260219000000_create_sinaliza_events.rb +20 -0
- data/db/migrate/20260219100000_add_parent_id_to_sinaliza_events.rb +5 -0
- data/lib/sinaliza/configuration.rb +12 -0
- data/lib/sinaliza/engine.rb +5 -0
- data/lib/sinaliza/version.rb +3 -0
- data/lib/sinaliza.rb +48 -0
- data/lib/tasks/sinaliza_tasks.rake +15 -0
- metadata +91 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9347558f2c9e741fe50da41cfd1f43835d03df264036a1d584f2f5e0806e598b
|
|
4
|
+
data.tar.gz: 86d025a67d8e219f120cb7f8e6b0631298a1b81ed20d4045096c4e3e9a7cb7a7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9f557f39a26baab6127cbc66cdb24920186ce4e8d43845b93448445204e9b933be9025662da4a18597365a06e3d0ff7f67ce37ef4990f22a8fb731919da26f5a
|
|
7
|
+
data.tar.gz: df3fdc8f2cf43839a962a9e6e4529e0c8ba407f59fda8899e4bdb9fa787cc4644f0a84770346e1bcfd05ad700f53ab5ff5148743bfa2fa13434dbe4e075cbb67
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Marcelo Moraes
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Sinaliza
|
|
2
|
+
|
|
3
|
+
A Rails engine for recording and browsing application events. Track user actions, system events, and anything worth logging — from models, controllers, or anywhere in your code.
|
|
4
|
+
|
|
5
|
+
Events are stored in the database and viewable through a mountable monitor dashboard.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "sinaliza", github: "marcelonmoraes/sinaliza"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then run:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
bin/rails sinaliza:install:migrations
|
|
20
|
+
bin/rails db:migrate
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Mount the engine in your `config/routes.rb`:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
mount Sinaliza::Engine => "/sinaliza"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### Direct recording
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# Synchronous
|
|
35
|
+
Sinaliza.record(name: "user.signed_up", actor: user, metadata: { plan: "pro" })
|
|
36
|
+
|
|
37
|
+
# Asynchronous (via ActiveJob)
|
|
38
|
+
Sinaliza.record_later(name: "report.generated", target: report)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Both methods accept:
|
|
42
|
+
|
|
43
|
+
| Parameter | Description | Default |
|
|
44
|
+
|-------------|--------------------------------------|-----------------------|
|
|
45
|
+
| `name` | Event name (required) | — |
|
|
46
|
+
| `actor` | Who performed the action (any model) | `nil` |
|
|
47
|
+
| `target` | What was acted upon (any model) | `nil` |
|
|
48
|
+
| `metadata` | Arbitrary hash of extra data | `{}` |
|
|
49
|
+
| `source` | Origin label | `"manual"` |
|
|
50
|
+
| `ip_address`| IP address | `nil` |
|
|
51
|
+
| `user_agent`| User agent string | `nil` |
|
|
52
|
+
| `request_id`| Request ID | `nil` |
|
|
53
|
+
|
|
54
|
+
### Model concern — `Sinaliza::Trackable`
|
|
55
|
+
|
|
56
|
+
Include in any model to get event associations and helper methods:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
class User < ApplicationRecord
|
|
60
|
+
include Sinaliza::Trackable
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This gives you:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
user.events_as_actor # events where user is the actor
|
|
68
|
+
user.events_as_target # events where user is the target
|
|
69
|
+
|
|
70
|
+
user.track_event("profile.updated", metadata: { field: "email" })
|
|
71
|
+
user.track_event("post.published", target: post)
|
|
72
|
+
|
|
73
|
+
post.track_event_as_target("post.featured", actor: admin)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Events are recorded with `source: "model"`. When an actor or target is destroyed, associated events are preserved with nullified references (`dependent: :nullify`).
|
|
77
|
+
|
|
78
|
+
### Controller concern — `Sinaliza::Traceable`
|
|
79
|
+
|
|
80
|
+
Include in any controller to track actions declaratively or manually:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
class OrdersController < ApplicationController
|
|
84
|
+
include Sinaliza::Traceable
|
|
85
|
+
|
|
86
|
+
# Declarative — runs as an after_action callback
|
|
87
|
+
track_event "orders.listed", only: :index
|
|
88
|
+
track_event "order.viewed", only: :show, metadata: -> { { order_id: params[:id] } }
|
|
89
|
+
|
|
90
|
+
# Conditional tracking
|
|
91
|
+
track_event "order.created", only: :create, if: -> { response.successful? }
|
|
92
|
+
|
|
93
|
+
def refund
|
|
94
|
+
order = Order.find(params[:id])
|
|
95
|
+
order.refund!
|
|
96
|
+
|
|
97
|
+
# Manual — call anywhere in an action
|
|
98
|
+
record_event("order.refunded", target: order, metadata: { reason: params[:reason] })
|
|
99
|
+
|
|
100
|
+
redirect_to order
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Events are recorded with `source: "controller"`. Request context (IP address, user agent, request ID) is captured automatically based on configuration.
|
|
106
|
+
|
|
107
|
+
The actor is resolved by calling the method defined in `Sinaliza.configuration.actor_method` (default: `current_user`).
|
|
108
|
+
|
|
109
|
+
### Query scopes
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
Sinaliza::Event.by_name("user.login")
|
|
113
|
+
Sinaliza::Event.by_source("controller")
|
|
114
|
+
Sinaliza::Event.by_actor_type("User")
|
|
115
|
+
Sinaliza::Event.since(1.week.ago)
|
|
116
|
+
Sinaliza::Event.before(Date.yesterday)
|
|
117
|
+
Sinaliza::Event.between(1.week.ago, 1.day.ago)
|
|
118
|
+
Sinaliza::Event.search("login")
|
|
119
|
+
Sinaliza::Event.chronological # oldest first
|
|
120
|
+
Sinaliza::Event.reverse_chronological # newest first
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Scopes are chainable:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
Sinaliza::Event.by_name("order.created").by_actor_type("User").since(1.day.ago)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Dashboard
|
|
130
|
+
|
|
131
|
+
The engine mounts a monitor dashboard at your chosen path. It provides:
|
|
132
|
+
|
|
133
|
+
- Paginated event list (cursor-based, 50 per page)
|
|
134
|
+
- Filtering by name, source, actor type, date range, and free-text search
|
|
135
|
+
- Detail view for each event with formatted JSON metadata
|
|
136
|
+
|
|
137
|
+
### Protecting the dashboard
|
|
138
|
+
|
|
139
|
+
The gem does not include authentication. Protect access via route constraints in your host app:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
# config/routes.rb
|
|
143
|
+
authenticate :user, ->(u) { u.admin? } do
|
|
144
|
+
mount Sinaliza::Engine => "/sinaliza"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# or with a simple constraint
|
|
148
|
+
mount Sinaliza::Engine => "/sinaliza", constraints: AdminConstraint.new
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Configuration
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# config/initializers/sinaliza.rb
|
|
155
|
+
Sinaliza.configure do |config|
|
|
156
|
+
# Controller method used to resolve the actor (default: :current_user)
|
|
157
|
+
config.actor_method = :current_user
|
|
158
|
+
|
|
159
|
+
# Default source label for Sinaliza.record calls (default: "manual")
|
|
160
|
+
config.default_source = "manual"
|
|
161
|
+
|
|
162
|
+
# Capture IP, user agent, and request ID in controller events (default: true)
|
|
163
|
+
config.record_request_info = true
|
|
164
|
+
|
|
165
|
+
# Auto-purge events older than this duration (default: nil — no purging)
|
|
166
|
+
config.purge_after = 90.days
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Purging old events
|
|
171
|
+
|
|
172
|
+
If `purge_after` is configured, run the rake task to delete old events:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
bin/rails sinaliza:purge
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Schedule it with cron, Heroku Scheduler, or whatever you prefer.
|
|
179
|
+
|
|
180
|
+
## Database schema
|
|
181
|
+
|
|
182
|
+
Events are stored in a single `sinaliza_events` table with polymorphic `actor` and `target` columns. The `metadata` column uses `json` type for cross-database compatibility (SQLite, PostgreSQL, MySQL).
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
.sinaliza-dashboard {
|
|
2
|
+
max-width: 1200px;
|
|
3
|
+
margin: 0 auto;
|
|
4
|
+
padding: 20px;
|
|
5
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
6
|
+
font-size: 14px;
|
|
7
|
+
color: #1a1a1a;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.sinaliza-dashboard h1 {
|
|
11
|
+
font-size: 24px;
|
|
12
|
+
font-weight: 600;
|
|
13
|
+
margin-bottom: 20px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* Filters */
|
|
17
|
+
.sinaliza-filters {
|
|
18
|
+
background: #f8f9fa;
|
|
19
|
+
border: 1px solid #dee2e6;
|
|
20
|
+
border-radius: 6px;
|
|
21
|
+
padding: 16px;
|
|
22
|
+
margin-bottom: 20px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.sinaliza-filters__row {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-wrap: wrap;
|
|
28
|
+
gap: 12px;
|
|
29
|
+
align-items: flex-end;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.sinaliza-filters__field {
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
gap: 4px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.sinaliza-filters__field label {
|
|
39
|
+
font-size: 12px;
|
|
40
|
+
font-weight: 600;
|
|
41
|
+
color: #495057;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.sinaliza-filters__field input,
|
|
45
|
+
.sinaliza-filters__field select {
|
|
46
|
+
padding: 6px 10px;
|
|
47
|
+
border: 1px solid #ced4da;
|
|
48
|
+
border-radius: 4px;
|
|
49
|
+
font-size: 13px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.sinaliza-filters__actions {
|
|
53
|
+
display: flex;
|
|
54
|
+
gap: 8px;
|
|
55
|
+
align-items: center;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.sinaliza-filters__actions button {
|
|
59
|
+
padding: 6px 16px;
|
|
60
|
+
background: #0d6efd;
|
|
61
|
+
color: white;
|
|
62
|
+
border: none;
|
|
63
|
+
border-radius: 4px;
|
|
64
|
+
font-size: 13px;
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.sinaliza-filters__actions button:hover {
|
|
69
|
+
background: #0b5ed7;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Table */
|
|
73
|
+
.sinaliza-table {
|
|
74
|
+
width: 100%;
|
|
75
|
+
border-collapse: collapse;
|
|
76
|
+
background: white;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.sinaliza-table th,
|
|
80
|
+
.sinaliza-table td {
|
|
81
|
+
padding: 10px 12px;
|
|
82
|
+
text-align: left;
|
|
83
|
+
border-bottom: 1px solid #dee2e6;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.sinaliza-table th {
|
|
87
|
+
font-weight: 600;
|
|
88
|
+
font-size: 12px;
|
|
89
|
+
color: #495057;
|
|
90
|
+
text-transform: uppercase;
|
|
91
|
+
letter-spacing: 0.5px;
|
|
92
|
+
background: #f8f9fa;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.sinaliza-table tbody tr:hover {
|
|
96
|
+
background: #f8f9fa;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.sinaliza-time {
|
|
100
|
+
white-space: nowrap;
|
|
101
|
+
color: #6c757d;
|
|
102
|
+
font-size: 13px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Detail table */
|
|
106
|
+
.sinaliza-detail {
|
|
107
|
+
width: 100%;
|
|
108
|
+
border-collapse: collapse;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.sinaliza-detail th,
|
|
112
|
+
.sinaliza-detail td {
|
|
113
|
+
padding: 10px 12px;
|
|
114
|
+
border-bottom: 1px solid #dee2e6;
|
|
115
|
+
vertical-align: top;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.sinaliza-detail th {
|
|
119
|
+
width: 140px;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
color: #495057;
|
|
122
|
+
text-align: right;
|
|
123
|
+
background: #f8f9fa;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Badges */
|
|
127
|
+
.sinaliza-badge {
|
|
128
|
+
display: inline-block;
|
|
129
|
+
padding: 2px 8px;
|
|
130
|
+
border-radius: 4px;
|
|
131
|
+
font-size: 12px;
|
|
132
|
+
font-weight: 500;
|
|
133
|
+
background: #e9ecef;
|
|
134
|
+
color: #495057;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.sinaliza-badge--source {
|
|
138
|
+
background: #d1ecf1;
|
|
139
|
+
color: #0c5460;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Links */
|
|
143
|
+
.sinaliza-link {
|
|
144
|
+
color: #0d6efd;
|
|
145
|
+
text-decoration: none;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.sinaliza-link:hover {
|
|
149
|
+
text-decoration: underline;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* JSON */
|
|
153
|
+
.sinaliza-json {
|
|
154
|
+
background: #f8f9fa;
|
|
155
|
+
padding: 10px;
|
|
156
|
+
border-radius: 4px;
|
|
157
|
+
font-size: 13px;
|
|
158
|
+
overflow-x: auto;
|
|
159
|
+
margin: 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* Empty state */
|
|
163
|
+
.sinaliza-empty {
|
|
164
|
+
text-align: center;
|
|
165
|
+
color: #6c757d;
|
|
166
|
+
padding: 40px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* Pagination */
|
|
170
|
+
.sinaliza-pagination {
|
|
171
|
+
padding: 16px 0;
|
|
172
|
+
text-align: center;
|
|
173
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Sinaliza
|
|
2
|
+
module Traceable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
class_methods do
|
|
6
|
+
def track_event(name, only: nil, except: nil, metadata: {}, if: nil)
|
|
7
|
+
callback_options = {}
|
|
8
|
+
callback_options[:only] = only if only
|
|
9
|
+
callback_options[:except] = except if except
|
|
10
|
+
callback_options[:if] = binding.local_variable_get(:if) if binding.local_variable_get(:if)
|
|
11
|
+
|
|
12
|
+
after_action(**callback_options) do
|
|
13
|
+
resolved_metadata = metadata.is_a?(Proc) ? instance_exec(&metadata) : metadata
|
|
14
|
+
record_event(name, metadata: resolved_metadata)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def record_event(name, target: nil, parent: nil, metadata: {})
|
|
22
|
+
actor = resolve_actor
|
|
23
|
+
|
|
24
|
+
attributes = {
|
|
25
|
+
name: name,
|
|
26
|
+
actor: actor,
|
|
27
|
+
target: target,
|
|
28
|
+
parent: parent,
|
|
29
|
+
metadata: metadata,
|
|
30
|
+
source: "controller"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if Sinaliza.configuration.record_request_info
|
|
34
|
+
attributes[:ip_address] = request.remote_ip
|
|
35
|
+
attributes[:user_agent] = request.user_agent
|
|
36
|
+
attributes[:request_id] = request.request_id
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Sinaliza.record(**attributes)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def resolve_actor
|
|
43
|
+
method = Sinaliza.configuration.actor_method
|
|
44
|
+
send(method) if respond_to?(method, true)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Sinaliza
|
|
2
|
+
class EventsController < ApplicationController
|
|
3
|
+
PER_PAGE = 50
|
|
4
|
+
|
|
5
|
+
def index
|
|
6
|
+
@events = Event.roots.reverse_chronological
|
|
7
|
+
|
|
8
|
+
apply_filters
|
|
9
|
+
apply_cursor_pagination
|
|
10
|
+
|
|
11
|
+
@filter_names = Event.distinct.pluck(:name).sort
|
|
12
|
+
@filter_sources = Event.distinct.pluck(:source).sort
|
|
13
|
+
@filter_actor_types = Event.where.not(actor_type: nil).distinct.pluck(:actor_type).sort
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def show
|
|
17
|
+
@event = Event.find(params[:id])
|
|
18
|
+
@children = @event.children.reverse_chronological
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def apply_filters
|
|
24
|
+
@events = @events.by_name(params[:name]) if params[:name].present?
|
|
25
|
+
@events = @events.by_source(params[:source]) if params[:source].present?
|
|
26
|
+
@events = @events.by_actor_type(params[:actor_type]) if params[:actor_type].present?
|
|
27
|
+
@events = @events.search(params[:q]) if params[:q].present?
|
|
28
|
+
@events = @events.since(Date.parse(params[:since])) if params[:since].present?
|
|
29
|
+
@events = @events.before(Date.parse(params[:before]).end_of_day) if params[:before].present?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def apply_cursor_pagination
|
|
33
|
+
@events = @events.where("sinaliza_events.id < ?", params[:before_id].to_i) if params[:before_id].present?
|
|
34
|
+
@events = @events.limit(PER_PAGE + 1).to_a
|
|
35
|
+
|
|
36
|
+
@has_next_page = @events.size > PER_PAGE
|
|
37
|
+
@events = @events.first(PER_PAGE)
|
|
38
|
+
@next_cursor = @events.last&.id if @has_next_page
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Sinaliza
|
|
2
|
+
class RecordEventJob < ApplicationJob
|
|
3
|
+
queue_as :default
|
|
4
|
+
|
|
5
|
+
def perform(attributes)
|
|
6
|
+
attributes = attributes.symbolize_keys
|
|
7
|
+
|
|
8
|
+
if attributes[:actor].is_a?(String)
|
|
9
|
+
attributes[:actor] = GlobalID::Locator.locate(attributes[:actor])
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
if attributes[:target].is_a?(String)
|
|
13
|
+
attributes[:target] = GlobalID::Locator.locate(attributes[:target])
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
Sinaliza::Event.create!(attributes)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Sinaliza
|
|
2
|
+
module Trackable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
has_many :events_as_actor,
|
|
7
|
+
class_name: "Sinaliza::Event",
|
|
8
|
+
as: :actor,
|
|
9
|
+
dependent: :nullify
|
|
10
|
+
|
|
11
|
+
has_many :events_as_target,
|
|
12
|
+
class_name: "Sinaliza::Event",
|
|
13
|
+
as: :target,
|
|
14
|
+
dependent: :nullify
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def track_event(name, target: nil, parent: nil, metadata: {})
|
|
18
|
+
Sinaliza.record(
|
|
19
|
+
name: name,
|
|
20
|
+
actor: self,
|
|
21
|
+
target: target,
|
|
22
|
+
parent: parent,
|
|
23
|
+
metadata: metadata,
|
|
24
|
+
source: "model"
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def track_event_as_target(name, actor: nil, parent: nil, metadata: {})
|
|
29
|
+
Sinaliza.record(
|
|
30
|
+
name: name,
|
|
31
|
+
actor: actor,
|
|
32
|
+
target: self,
|
|
33
|
+
parent: parent,
|
|
34
|
+
metadata: metadata,
|
|
35
|
+
source: "model"
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Sinaliza
|
|
2
|
+
class Event < ApplicationRecord
|
|
3
|
+
belongs_to :actor, polymorphic: true, optional: true
|
|
4
|
+
belongs_to :target, polymorphic: true, optional: true
|
|
5
|
+
belongs_to :parent, class_name: "Sinaliza::Event", optional: true
|
|
6
|
+
|
|
7
|
+
has_many :children, class_name: "Sinaliza::Event", foreign_key: :parent_id, dependent: :destroy
|
|
8
|
+
|
|
9
|
+
validates :name, presence: true
|
|
10
|
+
|
|
11
|
+
scope :by_name, ->(name) { where(name: name) }
|
|
12
|
+
scope :by_source, ->(source) { where(source: source) }
|
|
13
|
+
scope :by_actor_type, ->(type) { where(actor_type: type) }
|
|
14
|
+
scope :since, ->(time) { where(created_at: time..) }
|
|
15
|
+
scope :before, ->(time) { where(created_at: ..time) }
|
|
16
|
+
scope :between, ->(from, to) { where(created_at: from..to) }
|
|
17
|
+
scope :chronological, -> { order(created_at: :asc) }
|
|
18
|
+
scope :reverse_chronological, -> { order(created_at: :desc) }
|
|
19
|
+
scope :roots, -> { where(parent_id: nil) }
|
|
20
|
+
scope :search, ->(query) {
|
|
21
|
+
where("name LIKE :q OR source LIKE :q OR actor_type LIKE :q OR target_type LIKE :q", q: "%#{query}%")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def root?
|
|
25
|
+
parent_id.nil?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def child?
|
|
29
|
+
parent_id.present?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Sinaliza</title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
|
|
8
|
+
<%= yield :head %>
|
|
9
|
+
|
|
10
|
+
<%= stylesheet_link_tag "sinaliza/application", media: "all" %>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
|
|
14
|
+
<%= yield %>
|
|
15
|
+
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<form method="get" action="<%= events_path %>" class="sinaliza-filters">
|
|
2
|
+
<div class="sinaliza-filters__row">
|
|
3
|
+
<div class="sinaliza-filters__field">
|
|
4
|
+
<label for="q">Search</label>
|
|
5
|
+
<input type="text" name="q" id="q" value="<%= params[:q] %>" placeholder="Search events...">
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="sinaliza-filters__field">
|
|
9
|
+
<label for="name">Name</label>
|
|
10
|
+
<select name="name" id="name">
|
|
11
|
+
<option value="">All</option>
|
|
12
|
+
<% @filter_names.each do |name| %>
|
|
13
|
+
<option value="<%= name %>" <%= "selected" if params[:name] == name %>><%= name %></option>
|
|
14
|
+
<% end %>
|
|
15
|
+
</select>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="sinaliza-filters__field">
|
|
19
|
+
<label for="source">Source</label>
|
|
20
|
+
<select name="source" id="source">
|
|
21
|
+
<option value="">All</option>
|
|
22
|
+
<% @filter_sources.each do |source| %>
|
|
23
|
+
<option value="<%= source %>" <%= "selected" if params[:source] == source %>><%= source %></option>
|
|
24
|
+
<% end %>
|
|
25
|
+
</select>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="sinaliza-filters__field">
|
|
29
|
+
<label for="actor_type">Actor type</label>
|
|
30
|
+
<select name="actor_type" id="actor_type">
|
|
31
|
+
<option value="">All</option>
|
|
32
|
+
<% @filter_actor_types.each do |type| %>
|
|
33
|
+
<option value="<%= type %>" <%= "selected" if params[:actor_type] == type %>><%= type %></option>
|
|
34
|
+
<% end %>
|
|
35
|
+
</select>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="sinaliza-filters__field">
|
|
39
|
+
<label for="since">Since</label>
|
|
40
|
+
<input type="date" name="since" id="since" value="<%= params[:since] %>">
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="sinaliza-filters__field">
|
|
44
|
+
<label for="before">Before</label>
|
|
45
|
+
<input type="date" name="before" id="before" value="<%= params[:before] %>">
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="sinaliza-filters__actions">
|
|
49
|
+
<button type="submit">Filter</button>
|
|
50
|
+
<a href="<%= events_path %>" class="sinaliza-link">Clear</a>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</form>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<div class="sinaliza-dashboard">
|
|
2
|
+
<h1>Events Monitor</h1>
|
|
3
|
+
|
|
4
|
+
<%= render "filters" %>
|
|
5
|
+
|
|
6
|
+
<table class="sinaliza-table">
|
|
7
|
+
<thead>
|
|
8
|
+
<tr>
|
|
9
|
+
<th>Time</th>
|
|
10
|
+
<th>Name</th>
|
|
11
|
+
<th>Source</th>
|
|
12
|
+
<th>Actor</th>
|
|
13
|
+
<th>Target</th>
|
|
14
|
+
<th>Children</th>
|
|
15
|
+
<th></th>
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
<tbody>
|
|
19
|
+
<% @events.each do |event| %>
|
|
20
|
+
<tr>
|
|
21
|
+
<td class="sinaliza-time"><%= event.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
22
|
+
<td><span class="sinaliza-badge"><%= event.name %></span></td>
|
|
23
|
+
<td><span class="sinaliza-badge sinaliza-badge--source"><%= event.source %></span></td>
|
|
24
|
+
<td><%= event.actor ? "#{event.actor_type}##{event.actor_id}" : "-" %></td>
|
|
25
|
+
<td><%= event.target ? "#{event.target_type}##{event.target_id}" : "-" %></td>
|
|
26
|
+
<td><%= event.children.size %></td>
|
|
27
|
+
<td><%= link_to "Detail", event_path(event), class: "sinaliza-link" %></td>
|
|
28
|
+
</tr>
|
|
29
|
+
<% end %>
|
|
30
|
+
</tbody>
|
|
31
|
+
</table>
|
|
32
|
+
|
|
33
|
+
<% if @events.empty? %>
|
|
34
|
+
<p class="sinaliza-empty">No events found.</p>
|
|
35
|
+
<% end %>
|
|
36
|
+
|
|
37
|
+
<% if @has_next_page %>
|
|
38
|
+
<div class="sinaliza-pagination">
|
|
39
|
+
<%
|
|
40
|
+
pagination_params = request.query_parameters.merge(before_id: @next_cursor)
|
|
41
|
+
%>
|
|
42
|
+
<%= link_to "Older events", events_path(pagination_params), class: "sinaliza-link" %>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|
|
45
|
+
</div>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<div class="sinaliza-dashboard">
|
|
2
|
+
<p><%= link_to "Back to events", events_path, class: "sinaliza-link" %></p>
|
|
3
|
+
|
|
4
|
+
<h1>Event #<%= @event.id %></h1>
|
|
5
|
+
|
|
6
|
+
<table class="sinaliza-detail">
|
|
7
|
+
<tr>
|
|
8
|
+
<th>Name</th>
|
|
9
|
+
<td><span class="sinaliza-badge"><%= @event.name %></span></td>
|
|
10
|
+
</tr>
|
|
11
|
+
<% if @event.parent %>
|
|
12
|
+
<tr>
|
|
13
|
+
<th>Parent</th>
|
|
14
|
+
<td><%= link_to "Event ##{@event.parent_id}", event_path(@event.parent), class: "sinaliza-link" %></td>
|
|
15
|
+
</tr>
|
|
16
|
+
<% end %>
|
|
17
|
+
<tr>
|
|
18
|
+
<th>Source</th>
|
|
19
|
+
<td><span class="sinaliza-badge sinaliza-badge--source"><%= @event.source %></span></td>
|
|
20
|
+
</tr>
|
|
21
|
+
<tr>
|
|
22
|
+
<th>Actor</th>
|
|
23
|
+
<td><%= @event.actor ? "#{@event.actor_type}##{@event.actor_id}" : "-" %></td>
|
|
24
|
+
</tr>
|
|
25
|
+
<tr>
|
|
26
|
+
<th>Target</th>
|
|
27
|
+
<td><%= @event.target ? "#{@event.target_type}##{@event.target_id}" : "-" %></td>
|
|
28
|
+
</tr>
|
|
29
|
+
<tr>
|
|
30
|
+
<th>Metadata</th>
|
|
31
|
+
<td><pre class="sinaliza-json"><%= JSON.pretty_generate(@event.metadata) %></pre></td>
|
|
32
|
+
</tr>
|
|
33
|
+
<tr>
|
|
34
|
+
<th>IP address</th>
|
|
35
|
+
<td><%= @event.ip_address || "-" %></td>
|
|
36
|
+
</tr>
|
|
37
|
+
<tr>
|
|
38
|
+
<th>User agent</th>
|
|
39
|
+
<td><%= @event.user_agent || "-" %></td>
|
|
40
|
+
</tr>
|
|
41
|
+
<tr>
|
|
42
|
+
<th>Request ID</th>
|
|
43
|
+
<td><%= @event.request_id || "-" %></td>
|
|
44
|
+
</tr>
|
|
45
|
+
<tr>
|
|
46
|
+
<th>Created at</th>
|
|
47
|
+
<td><%= @event.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></td>
|
|
48
|
+
</tr>
|
|
49
|
+
</table>
|
|
50
|
+
|
|
51
|
+
<% if @children.any? %>
|
|
52
|
+
<h2>Sub-events</h2>
|
|
53
|
+
|
|
54
|
+
<table class="sinaliza-table">
|
|
55
|
+
<thead>
|
|
56
|
+
<tr>
|
|
57
|
+
<th>Time</th>
|
|
58
|
+
<th>Name</th>
|
|
59
|
+
<th>Source</th>
|
|
60
|
+
<th>Actor</th>
|
|
61
|
+
<th>Target</th>
|
|
62
|
+
<th></th>
|
|
63
|
+
</tr>
|
|
64
|
+
</thead>
|
|
65
|
+
<tbody>
|
|
66
|
+
<% @children.each do |child| %>
|
|
67
|
+
<tr>
|
|
68
|
+
<td class="sinaliza-time"><%= child.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
69
|
+
<td><span class="sinaliza-badge"><%= child.name %></span></td>
|
|
70
|
+
<td><span class="sinaliza-badge sinaliza-badge--source"><%= child.source %></span></td>
|
|
71
|
+
<td><%= child.actor ? "#{child.actor_type}##{child.actor_id}" : "-" %></td>
|
|
72
|
+
<td><%= child.target ? "#{child.target_type}##{child.target_id}" : "-" %></td>
|
|
73
|
+
<td><%= link_to "Detail", event_path(child), class: "sinaliza-link" %></td>
|
|
74
|
+
</tr>
|
|
75
|
+
<% end %>
|
|
76
|
+
</tbody>
|
|
77
|
+
</table>
|
|
78
|
+
<% end %>
|
|
79
|
+
</div>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class CreateSinalizaEvents < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :sinaliza_events do |t|
|
|
4
|
+
t.string :name, null: false
|
|
5
|
+
t.references :actor, polymorphic: true, index: true
|
|
6
|
+
t.references :target, polymorphic: true, index: true
|
|
7
|
+
t.json :metadata, default: {}
|
|
8
|
+
t.string :source, default: "manual"
|
|
9
|
+
t.string :ip_address
|
|
10
|
+
t.string :user_agent
|
|
11
|
+
t.string :request_id
|
|
12
|
+
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
add_index :sinaliza_events, :name
|
|
17
|
+
add_index :sinaliza_events, :source
|
|
18
|
+
add_index :sinaliza_events, :created_at
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Sinaliza
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :actor_method, :default_source, :record_request_info, :purge_after
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@actor_method = :current_user
|
|
7
|
+
@default_source = "manual"
|
|
8
|
+
@record_request_info = true
|
|
9
|
+
@purge_after = nil
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
data/lib/sinaliza.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require "sinaliza/version"
|
|
2
|
+
require "sinaliza/engine"
|
|
3
|
+
require "sinaliza/configuration"
|
|
4
|
+
|
|
5
|
+
module Sinaliza
|
|
6
|
+
class << self
|
|
7
|
+
def configuration
|
|
8
|
+
@configuration ||= Configuration.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def configure
|
|
12
|
+
yield(configuration)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def record(name:, actor: nil, target: nil, parent: nil, metadata: {}, source: nil, ip_address: nil, user_agent: nil, request_id: nil)
|
|
16
|
+
parent_id = parent.is_a?(Sinaliza::Event) ? parent.id : parent
|
|
17
|
+
|
|
18
|
+
Sinaliza::Event.create!(
|
|
19
|
+
name: name,
|
|
20
|
+
actor: actor,
|
|
21
|
+
target: target,
|
|
22
|
+
parent_id: parent_id,
|
|
23
|
+
metadata: metadata,
|
|
24
|
+
source: source || configuration.default_source,
|
|
25
|
+
ip_address: ip_address,
|
|
26
|
+
user_agent: user_agent,
|
|
27
|
+
request_id: request_id
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def record_later(name:, actor: nil, target: nil, parent: nil, metadata: {}, source: nil, ip_address: nil, user_agent: nil, request_id: nil)
|
|
32
|
+
attributes = {
|
|
33
|
+
name: name,
|
|
34
|
+
metadata: metadata,
|
|
35
|
+
source: source || configuration.default_source,
|
|
36
|
+
ip_address: ip_address,
|
|
37
|
+
user_agent: user_agent,
|
|
38
|
+
request_id: request_id
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
attributes[:actor] = actor.to_global_id.to_s if actor
|
|
42
|
+
attributes[:target] = target.to_global_id.to_s if target
|
|
43
|
+
attributes[:parent_id] = parent.is_a?(Sinaliza::Event) ? parent.id : parent if parent
|
|
44
|
+
|
|
45
|
+
Sinaliza::RecordEventJob.perform_later(attributes)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
namespace :sinaliza do
|
|
2
|
+
desc "Purge events older than Sinaliza.configuration.purge_after"
|
|
3
|
+
task purge: :environment do
|
|
4
|
+
purge_after = Sinaliza.configuration.purge_after
|
|
5
|
+
|
|
6
|
+
if purge_after.nil?
|
|
7
|
+
puts "No purge_after configured. Set Sinaliza.configuration.purge_after to a duration (e.g., 90.days)."
|
|
8
|
+
next
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
cutoff = purge_after.ago
|
|
12
|
+
count = Sinaliza::Event.where(created_at: ...cutoff).delete_all
|
|
13
|
+
puts "Purged #{count} events older than #{cutoff}."
|
|
14
|
+
end
|
|
15
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sinaliza
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Marcelo Moraes
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.1'
|
|
19
|
+
- - ">="
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 8.1.2
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - "~>"
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '8.1'
|
|
29
|
+
- - ">="
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: 8.1.2
|
|
32
|
+
description: Track user actions, system events, and anything worth logging — from
|
|
33
|
+
models, controllers, or anywhere in your code. Events are stored in the database
|
|
34
|
+
and viewable through a mountable monitor dashboard.
|
|
35
|
+
email:
|
|
36
|
+
- marcelonmoraes@gmail.com
|
|
37
|
+
executables: []
|
|
38
|
+
extensions: []
|
|
39
|
+
extra_rdoc_files: []
|
|
40
|
+
files:
|
|
41
|
+
- MIT-LICENSE
|
|
42
|
+
- README.md
|
|
43
|
+
- Rakefile
|
|
44
|
+
- app/assets/stylesheets/sinaliza/application.css
|
|
45
|
+
- app/controllers/concerns/sinaliza/traceable.rb
|
|
46
|
+
- app/controllers/sinaliza/application_controller.rb
|
|
47
|
+
- app/controllers/sinaliza/events_controller.rb
|
|
48
|
+
- app/helpers/sinaliza/application_helper.rb
|
|
49
|
+
- app/jobs/sinaliza/application_job.rb
|
|
50
|
+
- app/jobs/sinaliza/record_event_job.rb
|
|
51
|
+
- app/mailers/sinaliza/application_mailer.rb
|
|
52
|
+
- app/models/concerns/sinaliza/trackable.rb
|
|
53
|
+
- app/models/sinaliza/application_record.rb
|
|
54
|
+
- app/models/sinaliza/event.rb
|
|
55
|
+
- app/views/layouts/sinaliza/application.html.erb
|
|
56
|
+
- app/views/sinaliza/events/_filters.html.erb
|
|
57
|
+
- app/views/sinaliza/events/index.html.erb
|
|
58
|
+
- app/views/sinaliza/events/show.html.erb
|
|
59
|
+
- config/routes.rb
|
|
60
|
+
- db/migrate/20260219000000_create_sinaliza_events.rb
|
|
61
|
+
- db/migrate/20260219100000_add_parent_id_to_sinaliza_events.rb
|
|
62
|
+
- lib/sinaliza.rb
|
|
63
|
+
- lib/sinaliza/configuration.rb
|
|
64
|
+
- lib/sinaliza/engine.rb
|
|
65
|
+
- lib/sinaliza/version.rb
|
|
66
|
+
- lib/tasks/sinaliza_tasks.rake
|
|
67
|
+
homepage: https://github.com/marcelonmoraes/sinaliza
|
|
68
|
+
licenses:
|
|
69
|
+
- MIT
|
|
70
|
+
metadata:
|
|
71
|
+
homepage_uri: https://github.com/marcelonmoraes/sinaliza
|
|
72
|
+
source_code_uri: https://github.com/marcelonmoraes/sinaliza/tree/main
|
|
73
|
+
changelog_uri: https://github.com/marcelonmoraes/sinaliza/blob/main/CHANGELOG.md
|
|
74
|
+
rdoc_options: []
|
|
75
|
+
require_paths:
|
|
76
|
+
- lib
|
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - ">="
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '0'
|
|
87
|
+
requirements: []
|
|
88
|
+
rubygems_version: 3.7.2
|
|
89
|
+
specification_version: 4
|
|
90
|
+
summary: Rails engine for recording and browsing application events.
|
|
91
|
+
test_files: []
|