sqdash 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dbf32880457e6d0e0ee135bdedcd9c4011bcf4a446c195eaa18a296e2afc20c6
4
- data.tar.gz: e1dfaf39989e034e60c48932dc7a0599d0c3a1b42db79b7058e49b7325450f64
3
+ metadata.gz: 72ee9eb4f696be8766e6dbdbd24a75651674c2c7369a3d634f68600002eff350
4
+ data.tar.gz: 0bf7bc1d1834a98dc8a9db6640b64967e8e193a67a4f6550bd7fc5b601a68de3
5
5
  SHA512:
6
- metadata.gz: bf714d16853aab1ccdf74e756dfdb0477b459903fa5c6f20d8984673946562cb693c3f51bde983882a8add79107d44ecdce7fb872e7f49a1c63c77b812d1048e
7
- data.tar.gz: d06062de2d0d589685e015b8da084414460c38b9688069cf0b73ba2973704f6d33606cc3e1df7a609acea3ccd0fb8b5f9985a41e53d41055a16debf062838046
6
+ metadata.gz: 6e1396b021bc066a486d5b5be623bf76d058c123ada0184d253f05aa98b4996e194767db96dc8377311a1c8e6d2f6c6eb237acef48d545c1c9bc8698a15cf5a2
7
+ data.tar.gz: '007933f8ea693afcb433bb41ba1e72e274b0d8f1506a4e3dfc62dd52acfe447251604cc3be2668bb0872290453d515a894fcd915ff1f46a5824a7e01a29f015e'
data/.rubocop.yml ADDED
@@ -0,0 +1,71 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.2
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+ Exclude:
6
+ - "vendor/**/*"
7
+ - "bin/**/*"
8
+
9
+ Style/StringLiterals:
10
+ EnforcedStyle: double_quotes
11
+
12
+ Style/FrozenStringLiteralComment:
13
+ Enabled: true
14
+
15
+ Style/Documentation:
16
+ Enabled: false
17
+
18
+ Style/StringConcatenation:
19
+ Enabled: false
20
+
21
+ Layout/LineLength:
22
+ Max: 120
23
+ Exclude:
24
+ - "lib/sqdash/cli.rb"
25
+ - "lib/sqdash/renderer.rb"
26
+
27
+ Metrics/MethodLength:
28
+ Max: 30
29
+ Exclude:
30
+ - "lib/sqdash/cli.rb"
31
+ - "lib/sqdash/renderer.rb"
32
+ - "lib/sqdash/input_handler.rb"
33
+ - "lib/sqdash/autocomplete.rb"
34
+
35
+ Metrics/ClassLength:
36
+ Max: 300
37
+ Exclude:
38
+ - "lib/sqdash/cli.rb"
39
+ - "test/cli_test.rb"
40
+
41
+ Metrics/ModuleLength:
42
+ Max: 100
43
+ Exclude:
44
+ - "lib/sqdash/renderer.rb"
45
+ - "lib/sqdash/input_handler.rb"
46
+ - "lib/sqdash/autocomplete.rb"
47
+
48
+ Metrics/AbcSize:
49
+ Max: 30
50
+ Exclude:
51
+ - "lib/sqdash/cli.rb"
52
+ - "lib/sqdash/renderer.rb"
53
+ - "lib/sqdash/autocomplete.rb"
54
+
55
+ Metrics/CyclomaticComplexity:
56
+ Max: 15
57
+ Exclude:
58
+ - "lib/sqdash/cli.rb"
59
+ - "lib/sqdash/autocomplete.rb"
60
+
61
+ Metrics/PerceivedComplexity:
62
+ Max: 15
63
+ Exclude:
64
+ - "lib/sqdash/cli.rb"
65
+ - "lib/sqdash/renderer.rb"
66
+ - "lib/sqdash/autocomplete.rb"
67
+
68
+ Metrics/BlockLength:
69
+ Exclude:
70
+ - "test/**/*"
71
+ - "sqdash.gemspec"
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # sqdash
1
+ # sqdash: Solid Queue Terminal Dashboard
2
2
 
3
3
  A terminal dashboard for Rails 8's Solid Queue.
4
4
 
@@ -7,6 +7,7 @@ Solid Queue is the default Active Job backend in Rails 8, but it ships with no b
7
7
  ## Features
8
8
 
9
9
  - Live overview of all Solid Queue jobs with status, queue, and timestamps
10
+ - Paginated loading — handles 100k+ jobs without slowing down
10
11
  - View filters: all, failed, completed, pending
11
12
  - Sortable by created date or ID, ascending or descending
12
13
  - Fuzzy text filter across job class, queue name, and ID
@@ -32,7 +33,7 @@ bundle add sqdash
32
33
  sqdash connects directly to your Solid Queue database. You need:
33
34
 
34
35
  - A database with the Solid Queue schema (`solid_queue_*` tables) — PostgreSQL, MySQL, or SQLite
35
- - Ruby >= 3.0
36
+ - Ruby >= 3.2
36
37
  - The database adapter gem for your database:
37
38
 
38
39
  ```bash
@@ -62,11 +63,66 @@ sqdash sqlite3:///path/to/queue.db
62
63
  export DATABASE_URL=postgres://user:pass@localhost:5432/myapp_queue
63
64
  sqdash
64
65
 
66
+ # Use a specific config file
67
+ sqdash --config /path/to/config.yml
68
+
65
69
  # Falls back to default: postgres://sqd:sqd@localhost:5432/sqd_web_development_queue
66
70
  sqdash
67
71
  ```
68
72
 
69
- Connection priority: **CLI argument** > **`DATABASE_URL` env var** > **built-in default**.
73
+ Connection priority: **CLI argument** > **`DATABASE_URL` env var** > **`.sqdash.yml`** > **`~/.sqdash.yml`** > **built-in default**.
74
+
75
+ ### Config file
76
+
77
+ Instead of passing a database URL every time, create a `.sqdash.yml` file in your project directory or home directory:
78
+
79
+ ```yaml
80
+ # .sqdash.yml or ~/.sqdash.yml
81
+ database_url: postgres://user:pass@localhost:5432/myapp_queue
82
+ ```
83
+
84
+ A project-local `.sqdash.yml` takes precedence over `~/.sqdash.yml`. You can also specify a config file explicitly with `--config` / `-c`.
85
+
86
+ ## How to use
87
+
88
+ Once connected, sqdash shows a live dashboard of all your Solid Queue jobs. Here are common workflows:
89
+
90
+ ### Quick start
91
+
92
+ ```bash
93
+ # Connect to your Rails app's queue database
94
+ sqdash postgres://user:pass@localhost:5432/myapp_development
95
+
96
+ # Or if you have a .sqdash.yml config file, just run:
97
+ sqdash
98
+ ```
99
+
100
+ ### Investigating failed jobs
101
+
102
+ 1. Type `:view failed` and press `Enter` to show only failed jobs
103
+ 2. Use `↑`/`↓` to select a job
104
+ 3. Press `Enter` to see the full error backtrace and job arguments
105
+ 4. Press `r` to retry the job, or `d` to discard it
106
+ 5. Press `Esc` to go back to the list
107
+
108
+ ### Finding a specific job
109
+
110
+ 1. Press `/` to open the search filter
111
+ 2. Start typing a job class name (e.g., `UserMailer`), queue name, or job ID
112
+ 3. Press `Tab` to autocomplete — sqdash suggests matching class and queue names
113
+ 4. Press `Enter` to apply the filter
114
+ 5. Press `Esc` to clear the filter and see all jobs again
115
+
116
+ ### Sorting jobs
117
+
118
+ 1. Press `:` to open the command bar
119
+ 2. Type `sort created asc` to see oldest jobs first (useful for finding stuck jobs)
120
+ 3. Press `Enter` to apply
121
+ 4. Other options: `sort created desc` (default), `sort id asc`, `sort id desc`
122
+
123
+ ### Monitoring a queue in real time
124
+
125
+ sqdash auto-refreshes data every second when idle. You can also press `Space` to manually refresh. The stats bar at the top shows live counts of total, completed, failed, and pending jobs.
70
126
 
71
127
  ### Keyboard shortcuts
72
128
 
@@ -106,6 +162,7 @@ git clone https://github.com/nuhasami/sqdash.git
106
162
  cd sqdash
107
163
  bin/setup
108
164
  bundle exec ruby exe/sqdash
165
+ rake test
109
166
  ```
110
167
 
111
168
  ## Contributing
data/Rakefile CHANGED
@@ -1,4 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- task default: %i[]
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.test_files = FileList["test/**/*_test.rb"]
9
+ end
10
+
11
+ task default: :test
data/exe/sqdash CHANGED
@@ -1,3 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
2
4
  require "sqdash"
3
5
  Sqdash::CLI.start
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sqdash
4
+ module Autocomplete
5
+ def autocomplete_filter
6
+ return if @filter_text.empty?
7
+
8
+ query = @filter_text.downcase
9
+
10
+ candidates = (
11
+ Models::Job.distinct.pluck(:class_name) +
12
+ Models::Job.distinct.pluck(:queue_name)
13
+ ).uniq
14
+
15
+ matches = candidates.select { |c| c.downcase.start_with?(query) }
16
+
17
+ if matches.length == 1
18
+ @filter_text = matches.first
19
+ elsif matches.length > 1
20
+ @filter_text = common_prefix(matches)
21
+ end
22
+
23
+ load_data
24
+ end
25
+
26
+ def autocomplete_hint
27
+ return "" if @filter_text.empty?
28
+
29
+ query = @filter_text.downcase
30
+ candidates = (
31
+ Models::Job.distinct.pluck(:class_name) +
32
+ Models::Job.distinct.pluck(:queue_name)
33
+ ).uniq
34
+
35
+ matches = candidates.select { |c| c.downcase.start_with?(query) }
36
+
37
+ if matches.length == 1
38
+ matches.first[@filter_text.length..]
39
+ elsif matches.length > 1
40
+ prefix = common_prefix(matches)
41
+ remaining = prefix[@filter_text.length..] || ""
42
+ remaining + " (#{matches.length} matches)"
43
+ else
44
+ " (no matches)"
45
+ end
46
+ end
47
+
48
+ def autocomplete_command
49
+ return if @command_text.empty?
50
+
51
+ parts = @command_text.strip.split(/\s+/)
52
+ completing_new_word = @command_text.end_with?(" ")
53
+
54
+ if completing_new_word
55
+ case parts.length
56
+ when 1
57
+ subtree = CLI::COMMANDS[parts[0]]
58
+ return unless subtree.is_a?(Hash)
59
+
60
+ completed = complete_word("", subtree.keys)
61
+ @command_text = "#{parts[0]} #{completed}" if completed
62
+ when 2
63
+ subtree = CLI::COMMANDS.dig(parts[0], parts[1])
64
+ return unless subtree.is_a?(Array) && subtree.any?
65
+
66
+ completed = complete_word("", subtree)
67
+ @command_text = "#{parts[0]} #{parts[1]} #{completed}" if completed
68
+ end
69
+ else
70
+ case parts.length
71
+ when 1
72
+ completed = complete_word(parts[0], CLI::COMMANDS.keys)
73
+ @command_text = completed if completed
74
+ when 2
75
+ subtree = CLI::COMMANDS[parts[0]]
76
+ return unless subtree.is_a?(Hash)
77
+
78
+ completed = complete_word(parts[1], subtree.keys)
79
+ @command_text = "#{parts[0]} #{completed}" if completed
80
+ when 3
81
+ subtree = CLI::COMMANDS.dig(parts[0], parts[1])
82
+ return unless subtree.is_a?(Array) && subtree.any?
83
+
84
+ completed = complete_word(parts[2], subtree)
85
+ @command_text = "#{parts[0]} #{parts[1]} #{completed}" if completed
86
+ end
87
+ end
88
+ end
89
+
90
+ def command_autocomplete_hint
91
+ return "" if @command_text.empty?
92
+
93
+ parts = @command_text.strip.split(/\s+/)
94
+ completing_new_word = @command_text.end_with?(" ")
95
+
96
+ if completing_new_word
97
+ case parts.length
98
+ when 1
99
+ subtree = CLI::COMMANDS[parts[0]]
100
+ return "" unless subtree.is_a?(Hash)
101
+
102
+ hint_for_candidates("", subtree.keys)
103
+ when 2
104
+ subtree = CLI::COMMANDS.dig(parts[0], parts[1])
105
+ return "" unless subtree.is_a?(Array) && subtree.any?
106
+
107
+ hint_for_candidates("", subtree)
108
+ else
109
+ ""
110
+ end
111
+ else
112
+ case parts.length
113
+ when 1
114
+ hint_for_candidates(parts[0], CLI::COMMANDS.keys)
115
+ when 2
116
+ subtree = CLI::COMMANDS[parts[0]]
117
+ return "" unless subtree.is_a?(Hash)
118
+
119
+ hint_for_candidates(parts[1], subtree.keys)
120
+ when 3
121
+ subtree = CLI::COMMANDS.dig(parts[0], parts[1])
122
+ return "" unless subtree.is_a?(Array) && subtree.any?
123
+
124
+ hint_for_candidates(parts[2], subtree)
125
+ else
126
+ ""
127
+ end
128
+ end
129
+ end
130
+
131
+ def complete_word(partial, candidates)
132
+ matches = candidates.select { |c| c.downcase.start_with?(partial.downcase) }
133
+ if matches.length == 1
134
+ matches.first
135
+ elsif matches.length > 1
136
+ prefix = common_prefix(matches)
137
+ prefix.length > partial.length ? prefix : nil
138
+ end
139
+ end
140
+
141
+ def common_prefix(strings)
142
+ return "" if strings.empty?
143
+
144
+ prefix = strings.first
145
+ strings.each do |s|
146
+ prefix = prefix[0...prefix.length].chars.take_while.with_index { |c, i| s[i]&.downcase == c.downcase }.join
147
+ end
148
+ prefix
149
+ end
150
+
151
+ def hint_for_candidates(partial, candidates)
152
+ matches = candidates.select { |c| c.downcase.start_with?(partial.downcase) }
153
+ if matches.length == 1
154
+ matches.first[partial.length..]
155
+ elsif matches.length > 1
156
+ prefix = common_prefix(matches)
157
+ remaining = prefix[partial.length..] || ""
158
+ remaining + " (#{matches.join('|')})"
159
+ else
160
+ " (no matches)"
161
+ end
162
+ end
163
+ end
164
+ end