dbwatcher 0.1.5

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ee5502341a37e588b1dcf38edd5f33fa9e0b3746f43efea7e419ec241e873150
4
+ data.tar.gz: 874907249da682606c7f02df3e423ba3f0b6ef8c553ff6165561ab5cb7ad0981
5
+ SHA512:
6
+ metadata.gz: 8b8d78baaa87bad96ac04968238b945d162c8acc0451c678803d7defdfa46ab66fbb9d7e96ab8bc11a4f09166713f995e641bea32090c2346ab335939faa7770
7
+ data.tar.gz: 13dd481403397e95a74c04857718f878ebd35dc223ffa164dfaf871f6da4a9c0d02559555b25f5b575e5bab2cf265ce953902e400026647d3441c9bdd38a3e92
data/README.md ADDED
@@ -0,0 +1,282 @@
1
+ # DBWatcher ๐Ÿ”
2
+
3
+ Track and visualize database changes in your Rails application for easier debugging and development.
4
+
5
+ DBWatcher is a powerful Rails gem that captures, stores, and visualizes all database operations in your application. Perfect for debugging complex data flows, understanding application behavior, and optimizing database performance.
6
+
7
+ ## โœจ Features
8
+
9
+ - **๐Ÿ“Š Real-time Database Tracking**: Monitor all SQL operations (INSERT, UPDATE, DELETE, SELECT)
10
+ - **๐ŸŽฏ Selective Tracking**: Track specific code blocks or entire requests
11
+ - **๐Ÿ“ฑ Web Dashboard**: Beautiful, responsive interface built with Alpine.js and Tailwind CSS
12
+ - **๐Ÿ’พ File-based Storage**: No additional database setup required
13
+ - **๐Ÿ”— URL-based Activation**: Simple `?dbwatch=true` parameter enables tracking
14
+ - **๐Ÿงน Automatic Cleanup**: Configurable session cleanup and storage management
15
+ - **โšก Zero Dependencies**: Works with any Rails application without complex setup
16
+ - **๐Ÿ”’ Development-focused**: Designed for development and testing environments
17
+
18
+ ## ๐Ÿš€ Installation
19
+
20
+ Add to your Gemfile:
21
+
22
+ ```ruby
23
+ gem 'dbwatcher', group: :development
24
+ ```
25
+
26
+ Then run:
27
+
28
+ ```bash
29
+ bundle install
30
+ ```
31
+
32
+ The engine will automatically mount at `/dbwatcher` in your Rails application.
33
+
34
+ ### Manual Route Mounting (Optional)
35
+
36
+ If you need custom mounting, add to your `config/routes.rb`:
37
+
38
+ ```ruby
39
+ Rails.application.routes.draw do
40
+ mount Dbwatcher::Engine => "/dbwatcher" if Rails.env.development?
41
+ # ... your other routes
42
+ end
43
+ ```
44
+
45
+ ## ๐Ÿ“– Usage
46
+
47
+ ### ๐ŸŽฏ Targeted Tracking
48
+
49
+ Track specific code blocks with detailed context:
50
+
51
+ ```ruby
52
+ Dbwatcher.track(name: "User Registration Flow") do
53
+ user = User.create!(
54
+ name: "John Doe",
55
+ email: "john@example.com"
56
+ )
57
+
58
+ user.create_profile!(
59
+ bio: "Software Developer",
60
+ location: "San Francisco"
61
+ )
62
+
63
+ user.posts.create!(
64
+ title: "Welcome Post",
65
+ content: "Hello World!"
66
+ )
67
+ end
68
+ ```
69
+
70
+ ### ๐ŸŒ URL-based Tracking
71
+
72
+ Enable tracking for any request by adding `?dbwatch=true`:
73
+
74
+ ```
75
+ # Track a user show page
76
+ GET /users/123?dbwatch=true
77
+
78
+ # Track a form submission
79
+ POST /users?dbwatch=true
80
+
81
+ # Track API endpoints
82
+ GET /api/posts?dbwatch=true
83
+ ```
84
+
85
+ ### ๐Ÿ“Š View Tracking Results
86
+
87
+ Visit `/dbwatcher` in your Rails application to access the dashboard where you can:
88
+ - Browse all tracking sessions
89
+ - View detailed SQL queries and timing
90
+ - Analyze database operation patterns
91
+ - Monitor application performance
92
+
93
+ ## โš™๏ธ Configuration
94
+
95
+ Create an initializer for custom configuration:
96
+
97
+ ```ruby
98
+ # config/initializers/dbwatcher.rb
99
+ Dbwatcher.configure do |config|
100
+ # Storage location for tracking data
101
+ config.storage_path = Rails.root.join('tmp', 'dbwatcher')
102
+
103
+ # Enable/disable tracking (default: development only)
104
+ config.enabled = Rails.env.development?
105
+
106
+ # Maximum number of sessions to keep
107
+ config.max_sessions = 100
108
+
109
+ # Automatic cleanup after N days
110
+ config.auto_clean_after_days = 7
111
+
112
+ # Include query parameters in tracking
113
+ config.include_params = true
114
+
115
+ # Exclude certain SQL patterns
116
+ config.excluded_patterns = [
117
+ /SHOW TABLES/,
118
+ /DESCRIBE/
119
+ ]
120
+ end
121
+ ```
122
+
123
+ ## ๐Ÿ—๏ธ Development & Testing
124
+
125
+ This project includes a comprehensive dummy Rails application for testing and development.
126
+
127
+ ### Running Tests
128
+
129
+ ```bash
130
+ # Run all tests
131
+ bundle exec rake test
132
+
133
+ # Run specific test suites
134
+ bundle exec rake unit # Unit tests
135
+ bundle exec rake acceptance # Feature tests
136
+ bundle exec cucumber -p chrome # Browser tests
137
+ ```
138
+
139
+ ### Development Server
140
+
141
+ ```bash
142
+ # Start the dummy application
143
+ cd spec/dummy
144
+ bundle exec rails server -p 3001
145
+
146
+ # Visit the test interface
147
+ open http://localhost:3001
148
+
149
+ # Visit DBWatcher dashboard
150
+ open http://localhost:3001/dbwatcher
151
+ ```
152
+
153
+ ### Code Quality
154
+
155
+ ```bash
156
+ # Run linter
157
+ bundle exec rubocop
158
+
159
+ # Auto-fix issues
160
+ bundle exec rubocop --autocorrect
161
+
162
+ # Security analysis
163
+ bundle exec brakeman
164
+ ```
165
+
166
+ ## ๐Ÿ› ๏ธ Troubleshooting
167
+
168
+ ### Route Helper Errors
169
+
170
+ If you encounter `undefined method 'dbwatcher_sessions_path'`:
171
+
172
+ 1. **Restart your Rails server** after installing the gem
173
+ 2. **Check Rails version** - requires Rails 6.0+
174
+ 3. **Manual mounting** - add the mount line to your routes file
175
+
176
+ ### Performance Considerations
177
+
178
+ - DBWatcher is designed for development environments
179
+ - Disable in production using `config.enabled = false`
180
+ - Use targeted tracking for performance-sensitive operations
181
+ - Regular cleanup prevents storage bloat
182
+
183
+ ### Storage Location
184
+
185
+ - Default: `Rails.root/tmp/dbwatcher/`
186
+ - Files are JSON formatted for easy inspection
187
+ - Sessions auto-expire based on configuration
188
+
189
+ ## ๐Ÿ”ง Advanced Usage
190
+
191
+ ### Custom Metadata
192
+
193
+ Add context to your tracking sessions:
194
+
195
+ ```ruby
196
+ Dbwatcher.track(
197
+ name: "Complex Business Logic",
198
+ metadata: {
199
+ user_id: current_user.id,
200
+ feature_flag: "new_checkout",
201
+ version: "2.1.0"
202
+ }
203
+ ) do
204
+ # Your database operations
205
+ end
206
+ ```
207
+
208
+ ### Conditional Tracking
209
+
210
+ ```ruby
211
+ Dbwatcher.track(name: "Admin Operations") do
212
+ # This will only track if DBWatcher is enabled
213
+ User.where(admin: true).update_all(last_seen: Time.current)
214
+ end if Dbwatcher.enabled?
215
+ ```
216
+
217
+ ### Integration with Testing
218
+
219
+ ```ruby
220
+ # In your test suite
221
+ RSpec.describe "User Registration" do
222
+ it "creates user with proper associations" do
223
+ session_data = nil
224
+
225
+ Dbwatcher.track(name: "Test User Creation") do
226
+ user = create(:user)
227
+ expect(user.profile).to be_present
228
+ end
229
+
230
+ # Analyze the tracked operations
231
+ expect(Dbwatcher::Storage.last_session).to include_sql(/INSERT INTO users/)
232
+ end
233
+ end
234
+ ```
235
+
236
+ ## ๐Ÿ“ Project Structure
237
+
238
+ ```
239
+ dbwatcher/
240
+ โ”œโ”€โ”€ app/
241
+ โ”‚ โ”œโ”€โ”€ controllers/dbwatcher/ # Web interface controllers
242
+ โ”‚ โ””โ”€โ”€ views/dbwatcher/ # Dashboard templates
243
+ โ”œโ”€โ”€ config/
244
+ โ”‚ โ””โ”€โ”€ routes.rb # Engine routes
245
+ โ”œโ”€โ”€ lib/dbwatcher/
246
+ โ”‚ โ”œโ”€โ”€ configuration.rb # Configuration management
247
+ โ”‚ โ”œโ”€โ”€ engine.rb # Rails engine
248
+ โ”‚ โ”œโ”€โ”€ middleware.rb # Rack middleware
249
+ โ”‚ โ”œโ”€โ”€ storage.rb # File-based storage
250
+ โ”‚ โ””โ”€โ”€ tracker.rb # Core tracking logic
251
+ โ””โ”€โ”€ spec/
252
+ โ”œโ”€โ”€ dummy/ # Test Rails application
253
+ โ”œโ”€โ”€ acceptance/ # Feature tests
254
+ โ””โ”€โ”€ unit/ # Unit tests
255
+ ```
256
+
257
+ ## ๐Ÿค Contributing
258
+
259
+ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
260
+
261
+ 1. Fork the repository
262
+ 2. Create a feature branch: `git checkout -b feature/amazing-feature`
263
+ 3. Make your changes and add tests
264
+ 4. Ensure all tests pass: `bundle exec rake test`
265
+ 5. Run the linter: `bundle exec rubocop`
266
+ 6. Commit your changes: `git commit -am 'Add amazing feature'`
267
+ 7. Push to the branch: `git push origin feature/amazing-feature`
268
+ 8. Open a Pull Request
269
+
270
+ ## ๐Ÿ“ License
271
+
272
+ This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
273
+
274
+ ## ๐Ÿ™ Acknowledgments
275
+
276
+ - Built with Rails Engine architecture
277
+ - UI powered by Alpine.js and Tailwind CSS
278
+ - Inspired by debugging needs in complex Rails applications
279
+
280
+ ---
281
+
282
+ **Happy Debugging!** ๐Ÿ›โœจ
data/Rakefile ADDED
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ # Unit tests
7
+ RSpec::Core::RakeTask.new(:unit) do |task|
8
+ task.pattern = "spec/unit/**/*_spec.rb"
9
+ end
10
+
11
+ # Acceptance tests (Cucumber)
12
+ begin
13
+ require "cucumber/rake/task"
14
+
15
+ Cucumber::Rake::Task.new(:acceptance) do |task|
16
+ task.cucumber_opts = ["spec/acceptance/features", "--require", "spec/acceptance"]
17
+ end
18
+ rescue LoadError
19
+ desc "Cucumber not available"
20
+ task :acceptance do
21
+ puts "Cucumber not available. Install cucumber-rails gem to run acceptance tests."
22
+ end
23
+ end
24
+
25
+ # All tests
26
+ task spec: [:unit]
27
+ task test: %i[spec acceptance]
28
+
29
+ require "rubocop/rake_task"
30
+ RuboCop::RakeTask.new
31
+
32
+ # Security tasks
33
+ desc "Run bundle audit to check for security vulnerabilities"
34
+ task :bundle_audit do
35
+ require "bundler/audit/task"
36
+ Bundler::Audit::Task.new
37
+ end
38
+
39
+ desc "Run Brakeman security scanner"
40
+ task :brakeman do
41
+ require "brakeman"
42
+ result = Brakeman.run app_path: "spec/dummy", config_file: ".brakeman.yml"
43
+ if result.warnings.any? || result.errors.any?
44
+ puts "โŒ Brakeman found security issues"
45
+ exit 1
46
+ else
47
+ puts "โœ… Brakeman security check passed"
48
+ end
49
+ end
50
+
51
+ desc "Run all security checks"
52
+ task security: %i[bundle_audit brakeman]
53
+
54
+ # Default task
55
+ task default: %i[test rubocop security]
56
+
57
+ # Setup tasks
58
+ desc "Set up test database and environment"
59
+ task :test_setup do
60
+ Dir.chdir("spec/dummy") do
61
+ system("bundle install")
62
+ system("bundle exec rails db:drop db:create db:migrate RAILS_ENV=test")
63
+ end
64
+ end
65
+
66
+ desc "Start test server for manual testing"
67
+ task :test_server do
68
+ Dir.chdir("spec/dummy") do
69
+ ENV["BUNDLE_GEMFILE"] = File.expand_path("Gemfile", Dir.pwd)
70
+ system("bundle exec rails server -e test -p 3001")
71
+ end
72
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ class SessionsController < ActionController::Base
5
+ protect_from_forgery with: :exception
6
+ layout "dbwatcher/application"
7
+
8
+ def index
9
+ @sessions = Storage.all_sessions
10
+ end
11
+
12
+ def show
13
+ @session = Storage.load_session(params[:id])
14
+
15
+ respond_to do |format|
16
+ format.html
17
+ format.json { render json: @session.to_h }
18
+ end
19
+ end
20
+
21
+ def destroy_all
22
+ Dbwatcher.reset!
23
+ redirect_to root_path, notice: "All sessions cleared"
24
+ end
25
+
26
+ private
27
+
28
+ # Helper method to safely get the sessions path
29
+ def sessions_index_path
30
+ if respond_to?(:sessions_path)
31
+ sessions_path
32
+ else
33
+ "/dbwatcher"
34
+ end
35
+ end
36
+ helper_method :sessions_index_path
37
+ end
38
+ end
@@ -0,0 +1,37 @@
1
+ <div class="px-4 sm:px-0">
2
+ <h2 class="text-2xl font-bold mb-6">Tracking Sessions</h2>
3
+
4
+ <% if @sessions.empty? %>
5
+ <div class="bg-white overflow-hidden shadow rounded-lg p-6 text-center text-gray-500">
6
+ No tracking sessions yet. Start tracking with <code class="bg-gray-100 px-2 py-1 rounded">Dbwatcher.track { ... }</code>
7
+ </div>
8
+ <% else %>
9
+ <div class="bg-white shadow overflow-hidden sm:rounded-md">
10
+ <ul class="divide-y divide-gray-200">
11
+ <% @sessions.each do |session| %>
12
+ <li>
13
+ <%= link_to session_path(session[:id]), class: "block hover:bg-gray-50 px-4 py-4 sm:px-6" do %>
14
+ <div class="flex items-center justify-between">
15
+ <div class="flex-1">
16
+ <p class="text-sm font-medium text-indigo-600 truncate">
17
+ <%= session[:name] %>
18
+ </p>
19
+ <p class="mt-1 text-sm text-gray-600">
20
+ <%= Time.parse(session[:started_at]).strftime("%Y-%m-%d %H:%M:%S") %>
21
+ <span class="text-gray-400">โ€ข</span>
22
+ <%= session[:change_count] %> changes
23
+ </p>
24
+ </div>
25
+ <div class="ml-2 flex-shrink-0">
26
+ <svg class="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
27
+ <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
28
+ </svg>
29
+ </div>
30
+ </div>
31
+ <% end %>
32
+ </li>
33
+ <% end %>
34
+ </ul>
35
+ </div>
36
+ <% end %>
37
+ </div>
@@ -0,0 +1,150 @@
1
+ <div x-data="sessionViewer(<%= @session.to_h.to_json %>)" class="space-y-6">
2
+ <div class="flex justify-between items-center">
3
+ <h2 class="text-2xl font-bold" x-text="session.name"></h2>
4
+ <button @click="showFullRecords = !showFullRecords"
5
+ class="bg-gray-100 hover:bg-gray-200 px-4 py-2 rounded flex items-center gap-2">
6
+ <span x-text="showFullRecords ? 'Show Changes Only' : 'Show Full Records'"></span>
7
+ </button>
8
+ </div>
9
+
10
+ <!-- Summary -->
11
+ <div class="bg-white shadow rounded-lg p-6">
12
+ <h3 class="text-lg font-semibold mb-4">Summary</h3>
13
+ <div class="grid grid-cols-3 gap-4">
14
+ <template x-for="[key, count] in Object.entries(session.summary)" :key="key">
15
+ <div class="text-sm">
16
+ <span x-text="key.split(',')[0]" class="font-medium"></span>
17
+ <span x-text="key.split(',')[1]" :class="getOperationColor(key.split(',')[1])"></span>:
18
+ <span x-text="count"></span>
19
+ </div>
20
+ </template>
21
+ </div>
22
+ </div>
23
+
24
+ <!-- Changes by Table -->
25
+ <div class="bg-white shadow rounded-lg p-6">
26
+ <h3 class="text-lg font-semibold mb-4">Database Changes</h3>
27
+
28
+ <div class="space-y-2">
29
+ <template x-for="table in groupedChanges" :key="table.name">
30
+ <div class="border rounded">
31
+ <button @click="table.expanded = !table.expanded"
32
+ class="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50">
33
+ <div class="flex items-center gap-2">
34
+ <svg class="w-4 h-4 transition-transform" :class="{'rotate-90': table.expanded}" fill="currentColor" viewBox="0 0 20 20">
35
+ <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
36
+ </svg>
37
+ <span class="font-medium" x-text="table.name"></span>
38
+ <span class="text-sm text-gray-500" x-text="`(${table.changes.length} changes)`"></span>
39
+ </div>
40
+ </button>
41
+
42
+ <div x-show="table.expanded" x-collapse class="border-t">
43
+ <template x-for="record in table.records" :key="record.id">
44
+ <div class="border-b last:border-0">
45
+ <button @click="record.expanded = !record.expanded"
46
+ class="w-full px-6 py-2 flex items-center gap-2 hover:bg-gray-50 text-left">
47
+ <svg class="w-4 h-4 transition-transform" :class="{'rotate-90': record.expanded}" fill="currentColor" viewBox="0 0 20 20">
48
+ <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
49
+ </svg>
50
+ <span x-text="`Record #${record.id}`"></span>
51
+ </button>
52
+
53
+ <div x-show="record.expanded" x-collapse class="px-8 pb-4">
54
+ <template x-for="change in record.changes" :key="change.timestamp">
55
+ <div class="mt-3">
56
+ <div class="font-medium mb-2" :class="getOperationColor(change.operation)">
57
+ <span x-text="change.operation"></span> at
58
+ <span x-text="new Date(change.timestamp).toLocaleTimeString()"></span>
59
+ </div>
60
+
61
+ <div x-show="!showFullRecords" class="space-y-1">
62
+ <template x-for="col in change.changes" :key="col.column">
63
+ <div class="text-sm">
64
+ <span class="font-medium" x-text="col.column + ':'"></span>
65
+ <template x-if="col.old_value">
66
+ <span>
67
+ <span class="text-red-600 line-through" x-text="col.old_value"></span>
68
+ <span class="mx-1">โ†’</span>
69
+ </span>
70
+ </template>
71
+ <span class="text-green-600" x-text="col.new_value || 'null'"></span>
72
+ </div>
73
+ </template>
74
+ </div>
75
+
76
+ <div x-show="showFullRecords" class="bg-gray-50 p-3 rounded text-sm font-mono">
77
+ <pre x-text="JSON.stringify(change.record_snapshot, null, 2)"></pre>
78
+ </div>
79
+ </div>
80
+ </template>
81
+ </div>
82
+ </div>
83
+ </template>
84
+ </div>
85
+ </div>
86
+ </template>
87
+ </div>
88
+ </div>
89
+ </div>
90
+
91
+ <script>
92
+ function sessionViewer(sessionData) {
93
+ return {
94
+ session: sessionData,
95
+ showFullRecords: false,
96
+ groupedChanges: [],
97
+
98
+ init() {
99
+ // Calculate summary from changes
100
+ const summary = {};
101
+ this.session.changes.forEach(change => {
102
+ const key = `${change.table_name},${change.operation}`;
103
+ summary[key] = (summary[key] || 0) + 1;
104
+ });
105
+ this.session.summary = summary;
106
+
107
+ // Group changes by table and record
108
+ const tables = {};
109
+
110
+ this.session.changes.forEach(change => {
111
+ if (!tables[change.table_name]) {
112
+ tables[change.table_name] = {
113
+ name: change.table_name,
114
+ expanded: false,
115
+ changes: [],
116
+ records: {}
117
+ };
118
+ }
119
+
120
+ tables[change.table_name].changes.push(change);
121
+
122
+ if (!tables[change.table_name].records[change.record_id]) {
123
+ tables[change.table_name].records[change.record_id] = {
124
+ id: change.record_id,
125
+ expanded: false,
126
+ changes: []
127
+ };
128
+ }
129
+
130
+ tables[change.table_name].records[change.record_id].changes.push(change);
131
+ });
132
+
133
+ // Convert to array format
134
+ this.groupedChanges = Object.values(tables).map(table => ({
135
+ ...table,
136
+ records: Object.values(table.records)
137
+ }));
138
+ },
139
+
140
+ getOperationColor(operation) {
141
+ const colors = {
142
+ 'INSERT': 'text-green-600',
143
+ 'UPDATE': 'text-blue-600',
144
+ 'DELETE': 'text-red-600'
145
+ };
146
+ return colors[operation] || 'text-gray-600';
147
+ }
148
+ };
149
+ }
150
+ </script>
@@ -0,0 +1,34 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>DB Watcher</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ </head>
10
+ <body class="bg-gray-50">
11
+ <div class="min-h-screen">
12
+ <nav class="bg-white shadow">
13
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
14
+ <div class="flex justify-between h-16">
15
+ <div class="flex items-center">
16
+ <h1 class="text-xl font-semibold">๐Ÿ” DB Watcher</h1>
17
+ </div>
18
+ <div class="flex items-center space-x-4">
19
+ <%= link_to "Sessions", sessions_index_path, class: "text-gray-700 hover:text-gray-900" %>
20
+ <%= button_to "Reset All Sessions", destroy_all_sessions_path,
21
+ method: :delete,
22
+ data: { confirm: "Are you sure? This will delete all tracking data." },
23
+ class: "bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600" %>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </nav>
28
+
29
+ <main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
30
+ <%= yield %>
31
+ </main>
32
+ </div>
33
+ </body>
34
+ </html>
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "dbwatcher"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ require "irb"
11
+ IRB.start(__FILE__)