solid_queue_web 0.6.0 → 0.8.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +38 -6
  3. data/Rakefile +2 -2
  4. data/app/assets/stylesheets/solid_queue_web/_01_base.css +41 -0
  5. data/app/assets/stylesheets/solid_queue_web/_02_layout.css +133 -0
  6. data/app/assets/stylesheets/solid_queue_web/_03_stats.css +49 -0
  7. data/app/assets/stylesheets/solid_queue_web/_04_table.css +52 -0
  8. data/app/assets/stylesheets/solid_queue_web/_05_badges.css +27 -0
  9. data/app/assets/stylesheets/solid_queue_web/_06_buttons.css +38 -0
  10. data/app/assets/stylesheets/solid_queue_web/_07_forms.css +103 -0
  11. data/app/assets/stylesheets/solid_queue_web/_08_detail.css +84 -0
  12. data/app/assets/stylesheets/solid_queue_web/_09_pagination.css +27 -0
  13. data/app/assets/stylesheets/solid_queue_web/_10_responsive.css +73 -0
  14. data/app/assets/stylesheets/solid_queue_web/_11_throughput.css +68 -0
  15. data/app/assets/stylesheets/solid_queue_web/_12_dark_mode.css +34 -0
  16. data/app/assets/stylesheets/solid_queue_web/application.css +1 -617
  17. data/app/controllers/solid_queue_web/application_controller.rb +2 -0
  18. data/app/controllers/solid_queue_web/dashboard_controller.rb +28 -0
  19. data/app/controllers/solid_queue_web/failed_jobs_controller.rb +26 -22
  20. data/app/controllers/solid_queue_web/history_controller.rb +35 -0
  21. data/app/controllers/solid_queue_web/jobs_controller.rb +38 -23
  22. data/app/controllers/solid_queue_web/queues/jobs_controller.rb +1 -1
  23. data/app/controllers/solid_queue_web/queues_controller.rb +15 -0
  24. data/app/controllers/solid_queue_web/retry_failed_jobs_controller.rb +31 -0
  25. data/app/controllers/solid_queue_web/search_controller.rb +1 -3
  26. data/app/helpers/solid_queue_web/application_helper.rb +15 -1
  27. data/app/javascript/solid_queue_web/application.js +2 -0
  28. data/app/javascript/solid_queue_web/refresh_controller.js +3 -2
  29. data/app/javascript/solid_queue_web/theme_controller.js +26 -0
  30. data/app/views/layouts/solid_queue_web/application.html.erb +12 -7
  31. data/app/views/solid_queue_web/dashboard/index.html.erb +66 -6
  32. data/app/views/solid_queue_web/failed_jobs/index.html.erb +2 -1
  33. data/app/views/solid_queue_web/history/index.html.erb +73 -0
  34. data/app/views/solid_queue_web/jobs/index.html.erb +11 -8
  35. data/app/views/solid_queue_web/processes/index.html.erb +1 -1
  36. data/app/views/solid_queue_web/queues/index.html.erb +15 -1
  37. data/app/views/solid_queue_web/search/index.html.erb +2 -2
  38. data/config/importmap.rb +1 -0
  39. data/config/routes.rb +15 -12
  40. data/lib/solid_queue_web/engine.rb +4 -2
  41. data/lib/solid_queue_web/version.rb +1 -1
  42. data/lib/solid_queue_web.rb +22 -0
  43. metadata +31 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69555db4ec8b786eb891721bf40655ef341c3b40b84b82b64cca69f446a511b8
4
- data.tar.gz: eafe8f169f2382cdb3e0f59b87e3fc5553b1637dadb31ad076caf50233563d9d
3
+ metadata.gz: 6897882e9e9b87a677fe9c6af855b33395cbf4bf2d13fb55086f461d26255f7d
4
+ data.tar.gz: 1b6882dce3a746811ba9553d0fb2174257c3f13146d5a448fd793c8c60a76661
5
5
  SHA512:
6
- metadata.gz: a1b63a6625cc58a9280ca325d829c8f395130f102494f03e346d84e8d8d769e2ebbbe69740df756a2b84a8f95ff04ca27b82c82e2d8924f78a952bc2e85a12d4
7
- data.tar.gz: 0f723d410742b889756f36c6d5ddef8d8524733e1149bc55826afbaf37f3009c54c238a593495e8e097e37a8ca67988d25b06bf8507bf3903791f06a67907fbd
6
+ metadata.gz: d19bc85fc4350e8ecb194111c87a7b882d1e5929142ec94211bcf6fef23840810ec4eb51ae4d957a3c2288263bba9955aabca9ea1874f4d3b7f6d34590f54a28
7
+ data.tar.gz: 3516ec14c55cd3839bf22d721a58d6a68016b0891b09088e5463e8f2e61e611b77d9eaca461ea49c6ffc225873589880b678fb829b53a877683e32de3d75456b
data/README.md CHANGED
@@ -33,8 +33,8 @@ SolidQueueWeb surfaces all of this in a browser UI available at any route you ch
33
33
 
34
34
  ## Features
35
35
 
36
- - **Dashboard** — stat cards showing counts for ready, scheduled, running, blocked, and failed jobs, plus queues, recurring tasks, and processes; auto-refreshes every 5 seconds
37
- - **Queues** — all queues sorted by name with size, latency, and pause/resume controls
36
+ - **Dashboard** — stat cards showing counts for ready, scheduled, running, blocked, and failed jobs, plus queues, recurring tasks, and processes; "Done (1h)" and "Done (24h)" throughput cards; a "Throughput — Last 12 Hours" bar chart showing completed-job counts per hour (pure CSS, no charting library); auto-refreshes every 5 seconds
37
+ - **Queues** — all queues sorted by name with size; oldest ready job latency (color-coded, with UTC timestamp tooltip); Done (24h) and Failed (24h) throughput counts; pause/resume controls
38
38
  - **Jobs** — filterable by status (ready, scheduled, claimed, blocked, failed) and by queue; search by job class name with dynamic auto-submit; time-based period filter (1 h / 24 h / 7 d); discard individual or all jobs; Turbo Frame navigation so only the table updates on filter or search; auto-refreshes every 10 seconds
39
39
  - **Failed jobs** — list of failed executions with error details; search by class name; filter by queue; time-based period filter; retry or discard individually or in bulk
40
40
  - **Job detail** — full arguments, timestamps, blocked-until date, and error backtrace; action buttons based on job status
@@ -43,6 +43,10 @@ SolidQueueWeb surfaces all of this in a browser UI available at any route you ch
43
43
  - **Processes** — workers, dispatchers, and supervisors with heartbeat health status; auto-refreshes every 10 seconds
44
44
  - **Global search** — search across all job statuses at once by class name substring; results grouped by status with match count and direct links to filtered views; native datalist autocomplete pre-populated from all known job classes; auto-submits on selection
45
45
  - **Targeted bulk actions** — checkboxes on the jobs and failed jobs lists for selecting individual rows; selection bar shows count and action buttons ("Discard Selected" for jobs, "Retry Selected" / "Discard Selected" for failed jobs); select-all checkbox in the table header
46
+ - **Job history** — browsable list of all finished jobs with class name, queue, duration, and finished timestamp; filterable by period (1h / 24h / 7d), queue, and class name search; Done (1h) / Done (24h) dashboard cards link directly to the filtered history view; auto-refreshes every 10 seconds
47
+ - **Dark mode** — ☽/☀ toggle in the header; preference persists to `localStorage` and defaults to the OS `prefers-color-scheme` on first visit; zero extra dependencies — implemented via CSS custom properties and a small Stimulus controller
48
+ - **Dashboard quick actions** — "Retry All Failed" and "Discard All Blocked" cards appear on the dashboard only when the respective count is non-zero; one-click bulk operations with confirm dialogs, keeping the dashboard clean when everything is healthy
49
+ - **CSV export** — "Export CSV" button on the jobs, failed jobs, and history pages downloads all records matching the current filters; columns are tailored per view
46
50
 
47
51
  ## Screenshots
48
52
 
@@ -80,13 +84,20 @@ Add to your `config/routes.rb`:
80
84
  mount SolidQueueWeb::Engine, at: "/jobs"
81
85
  ```
82
86
 
83
- The dashboard will be available at `/jobs`. See [Authentication](#authentication) to restrict access to admin users.
87
+ The dashboard will be available at `/jobs`.
84
88
 
85
- ## Authentication
89
+ ## Configuration
86
90
 
87
- The engine ships with no authentication by default. Add a block to an initializer (e.g. `config/initializers/solid_queue_web.rb`) to protect the dashboard:
91
+ All settings are optional the dashboard works with zero configuration. Create `config/initializers/solid_queue_web.rb` to customize behavior:
88
92
 
89
93
  ```ruby
94
+ SolidQueueWeb.configure do |config|
95
+ config.page_size = 50 # rows per page across all paginated views (default: 25)
96
+ config.dashboard_refresh_interval = 10_000 # dashboard auto-refresh in ms (default: 5_000)
97
+ config.default_refresh_interval = 30_000 # jobs/processes/history auto-refresh in ms (default: 10_000)
98
+ config.search_results_limit = 10 # max results per status in global search (default: 25)
99
+ end
100
+
90
101
  SolidQueueWeb.authenticate do
91
102
  # Called in the context of ApplicationController — use any helper available there.
92
103
  # Return a truthy value to allow access, falsy to deny (triggers HTTP Basic prompt).
@@ -94,7 +105,28 @@ SolidQueueWeb.authenticate do
94
105
  end
95
106
  ```
96
107
 
97
- HTTP Basic authentication is used as a fallback when the block returns falsy.
108
+ No authentication is enforced by default. When the `authenticate` block returns falsy, HTTP Basic auth is used as a fallback.
109
+
110
+ ## Roadmap
111
+
112
+ Planned features, roughly ordered by priority:
113
+
114
+ **Observability**
115
+ - Job failure rate chart — sparkline per queue showing failure percentage over time, mirroring the throughput chart
116
+ - Queue depth trend — historical queue size over time, not just the current snapshot
117
+ - Slow job detection — flag jobs exceeding a configurable duration threshold
118
+
119
+ **Operations**
120
+ - Scheduled job management — reschedule a job to run immediately, or push its `scheduled_at` forward
121
+ - Bulk retry with delay — retry all failed jobs with a configurable stagger to avoid thundering herd
122
+ - Admin audit log — record who retried or discarded which jobs and when (requires host-app user identity)
123
+
124
+ **Infrastructure**
125
+ - Webhook / alert config — POST to a URL when the failure count exceeds a threshold
126
+ - Multi-database support — when Solid Queue runs on a separate database from the host app
127
+ - Read replica support — route dashboard queries to a replica to avoid impacting the primary
128
+
129
+ Pull requests for any of these are welcome. See [Contributing](#contributing) below.
98
130
 
99
131
  ## Contributing
100
132
 
data/Rakefile CHANGED
@@ -7,7 +7,7 @@ require "rspec/core/rake_task"
7
7
  RuboCop::RakeTask.new
8
8
  RSpec::Core::RakeTask.new(:spec)
9
9
 
10
- task default: [ :rubocop, :spec ]
10
+ task default: [:rubocop, :spec]
11
11
 
12
12
  namespace :dev do
13
13
  def dummy_env
@@ -32,5 +32,5 @@ namespace :dev do
32
32
  end
33
33
 
34
34
  desc "Reset and reseed the dummy app development database"
35
- task reset: [ :setup, :seed ]
35
+ task reset: [:setup, :seed]
36
36
  end
@@ -0,0 +1,41 @@
1
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2
+
3
+ .sqd-sr-only {
4
+ position: absolute;
5
+ width: 1px;
6
+ height: 1px;
7
+ padding: 0;
8
+ margin: -1px;
9
+ overflow: hidden;
10
+ clip: rect(0, 0, 0, 0);
11
+ white-space: nowrap;
12
+ border: 0;
13
+ }
14
+
15
+ :focus-visible {
16
+ outline: 2px solid var(--primary);
17
+ outline-offset: 2px;
18
+ border-radius: 2px;
19
+ }
20
+
21
+ :root {
22
+ --bg: #f8f9fa;
23
+ --surface: #ffffff;
24
+ --border: #dee2e6;
25
+ --text: #212529;
26
+ --muted: #6c757d;
27
+ --primary: #0d6efd;
28
+ --danger: #dc3545;
29
+ --warning: #fd7e14;
30
+ --success: #198754;
31
+ --info: #0dcaf0;
32
+ --purple: #6f42c1;
33
+ }
34
+
35
+ body {
36
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
37
+ font-size: 14px;
38
+ background: var(--bg);
39
+ color: var(--text);
40
+ line-height: 1.5;
41
+ }
@@ -0,0 +1,133 @@
1
+ .sqd-header {
2
+ background: var(--surface);
3
+ border-bottom: 1px solid var(--border);
4
+ }
5
+
6
+ .sqd-header__inner {
7
+ max-width: 1200px;
8
+ margin: 0 auto;
9
+ padding: 0 1.5rem;
10
+ display: flex;
11
+ align-items: center;
12
+ gap: 2rem;
13
+ height: 56px;
14
+ }
15
+
16
+ .sqd-header__controls {
17
+ display: flex;
18
+ align-items: center;
19
+ gap: 0.5rem;
20
+ margin-left: auto;
21
+ }
22
+
23
+ .sqd-header__title {
24
+ font-size: 16px;
25
+ font-weight: 600;
26
+ color: var(--text);
27
+ text-decoration: none;
28
+ }
29
+
30
+ .sqd-nav {
31
+ display: flex;
32
+ gap: 0.25rem;
33
+ list-style: none;
34
+ }
35
+
36
+ .sqd-nav a {
37
+ display: block;
38
+ padding: 0.35rem 0.75rem;
39
+ border-radius: 6px;
40
+ color: var(--muted);
41
+ text-decoration: none;
42
+ font-size: 13px;
43
+ font-weight: 500;
44
+ transition: background 0.1s, color 0.1s;
45
+ }
46
+
47
+ .sqd-nav a:hover,
48
+ .sqd-nav a.active {
49
+ background: var(--bg);
50
+ color: var(--text);
51
+ }
52
+
53
+ .sqd-theme-toggle {
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+ width: 32px;
58
+ height: 32px;
59
+ padding: 0;
60
+ background: none;
61
+ border: 1px solid var(--border);
62
+ border-radius: 5px;
63
+ cursor: pointer;
64
+ font-size: 16px;
65
+ color: var(--text);
66
+ line-height: 1;
67
+ flex-shrink: 0;
68
+ transition: background 0.1s, border-color 0.1s;
69
+ }
70
+
71
+ .sqd-theme-toggle:hover {
72
+ background: var(--bg);
73
+ }
74
+
75
+ .sqd-nav-toggle {
76
+ display: none;
77
+ flex-direction: column;
78
+ justify-content: center;
79
+ gap: 5px;
80
+ width: 36px;
81
+ height: 36px;
82
+ padding: 6px;
83
+ background: none;
84
+ border: 1px solid var(--border);
85
+ border-radius: 5px;
86
+ cursor: pointer;
87
+ }
88
+
89
+ .sqd-nav-toggle span {
90
+ display: block;
91
+ height: 2px;
92
+ background: var(--text);
93
+ border-radius: 1px;
94
+ }
95
+
96
+ .sqd-main {
97
+ max-width: 1200px;
98
+ margin: 0 auto;
99
+ padding: 2rem 1.5rem;
100
+ }
101
+
102
+ .sqd-page-title {
103
+ font-size: 20px;
104
+ font-weight: 600;
105
+ margin-bottom: 0;
106
+ }
107
+
108
+ .sqd-page-header {
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: space-between;
112
+ margin-bottom: 1.5rem;
113
+ }
114
+
115
+ .sqd-actions {
116
+ display: flex;
117
+ gap: 0.5rem;
118
+ }
119
+
120
+ @keyframes sqd-flash-dismiss {
121
+ 0%, 75% { opacity: 1; max-height: 120px; padding: 0.75rem 1rem; margin-bottom: 1rem; }
122
+ 100% { opacity: 0; max-height: 0; padding: 0; margin-bottom: 0; overflow: hidden; }
123
+ }
124
+
125
+ .sqd-flash {
126
+ padding: 0.75rem 1rem;
127
+ border-radius: 6px;
128
+ margin-bottom: 1rem;
129
+ font-size: 13px;
130
+ animation: sqd-flash-dismiss 6s ease-in forwards;
131
+ }
132
+ .sqd-flash--notice { background: #d1e7dd; color: #0f5132; border: 1px solid #badbcc; }
133
+ .sqd-flash--alert { background: #f8d7da; color: #842029; border: 1px solid #f5c2c7; }
@@ -0,0 +1,49 @@
1
+ .sqd-stats {
2
+ display: grid;
3
+ grid-template-columns: repeat(auto-fill, minmax(128px, 1fr));
4
+ gap: 1rem;
5
+ margin-bottom: 2rem;
6
+ }
7
+
8
+ .sqd-stat {
9
+ background: var(--surface);
10
+ border: 1px solid var(--border);
11
+ border-radius: 8px;
12
+ padding: 1.25rem 1rem;
13
+ text-align: center;
14
+ }
15
+
16
+ .sqd-stat__value {
17
+ font-size: 28px;
18
+ font-weight: 700;
19
+ line-height: 1;
20
+ margin-bottom: 0.25rem;
21
+ }
22
+
23
+ .sqd-stat__label {
24
+ font-size: 12px;
25
+ color: var(--muted);
26
+ text-transform: uppercase;
27
+ letter-spacing: 0.05em;
28
+ }
29
+
30
+ .sqd-stat--ready .sqd-stat__value { color: var(--success); }
31
+ .sqd-stat--scheduled .sqd-stat__value { color: var(--info); }
32
+ .sqd-stat--claimed .sqd-stat__value { color: var(--primary); }
33
+ .sqd-stat--failed .sqd-stat__value { color: var(--danger); }
34
+ .sqd-stat--blocked .sqd-stat__value { color: var(--warning); }
35
+ .sqd-stat--queues .sqd-stat__value { color: var(--purple); }
36
+ .sqd-stat--processes .sqd-stat__value { color: var(--muted); }
37
+ .sqd-stat--recurring .sqd-stat__value { color: var(--info); }
38
+
39
+ .sqd-stat--link {
40
+ display: block;
41
+ text-decoration: none;
42
+ color: inherit;
43
+ transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s;
44
+ }
45
+ .sqd-stat--link:hover {
46
+ border-color: var(--primary);
47
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
48
+ transform: translateY(-2px);
49
+ }
@@ -0,0 +1,52 @@
1
+ .sqd-card {
2
+ background: var(--surface);
3
+ border: 1px solid var(--border);
4
+ border-radius: 8px;
5
+ overflow: hidden;
6
+ }
7
+
8
+ .sqd-card__header {
9
+ padding: 0.875rem 1rem;
10
+ border-bottom: 1px solid var(--border);
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: space-between;
14
+ gap: 1rem;
15
+ }
16
+
17
+ .sqd-card__title {
18
+ font-size: 14px;
19
+ font-weight: 600;
20
+ }
21
+
22
+ table {
23
+ width: 100%;
24
+ border-collapse: collapse;
25
+ }
26
+
27
+ th {
28
+ padding: 0.625rem 1rem;
29
+ text-align: left;
30
+ font-size: 12px;
31
+ font-weight: 600;
32
+ color: var(--muted);
33
+ text-transform: uppercase;
34
+ letter-spacing: 0.05em;
35
+ border-bottom: 1px solid var(--border);
36
+ white-space: nowrap;
37
+ }
38
+
39
+ td {
40
+ padding: 0.75rem 1rem;
41
+ border-bottom: 1px solid var(--border);
42
+ vertical-align: middle;
43
+ }
44
+
45
+ tr:last-child td { border-bottom: none; }
46
+ tbody tr:hover { background: var(--bg); }
47
+
48
+ .sqd-empty {
49
+ text-align: center;
50
+ padding: 3rem 1rem;
51
+ color: var(--muted);
52
+ }
@@ -0,0 +1,27 @@
1
+ .sqd-badge {
2
+ display: inline-block;
3
+ padding: 0.2em 0.55em;
4
+ border-radius: 4px;
5
+ font-size: 11px;
6
+ font-weight: 600;
7
+ line-height: 1;
8
+ text-transform: uppercase;
9
+ letter-spacing: 0.04em;
10
+ }
11
+
12
+ .sqd-badge--ready { background: #d1e7dd; color: #0f5132; }
13
+ .sqd-badge--scheduled { background: #cff4fc; color: #055160; }
14
+ .sqd-badge--claimed { background: #cfe2ff; color: #084298; }
15
+ .sqd-badge--failed { background: #f8d7da; color: #842029; }
16
+ .sqd-badge--blocked { background: #fff3cd; color: #664d03; }
17
+ .sqd-badge--static { background: #d1e7dd; color: #0f5132; }
18
+ .sqd-badge--dynamic { background: #e0d7f5; color: #4a2c8a; }
19
+ .sqd-badge--paused { background: #e2e3e5; color: #41464b; }
20
+ .sqd-badge--running { background: #d1e7dd; color: #0f5132; }
21
+ .sqd-badge--supervisor { background: #e0d7f5; color: #4a2c8a; }
22
+ .sqd-badge--worker { background: #d1e7dd; color: #0f5132; }
23
+ .sqd-badge--dispatcher { background: #cff4fc; color: #055160; }
24
+
25
+ .sqd-process-meta { font-size: 12px; color: var(--muted); }
26
+ .sqd-process-meta span + span::before { content: " · "; }
27
+ .sqd-muted-text { color: var(--muted); font-size: 13px; }
@@ -0,0 +1,38 @@
1
+ .sqd-btn {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ padding: 0.35rem 0.75rem;
5
+ border-radius: 5px;
6
+ font-size: 12px;
7
+ font-weight: 500;
8
+ text-decoration: none;
9
+ border: 1px solid transparent;
10
+ cursor: pointer;
11
+ transition: opacity 0.15s;
12
+ }
13
+ .sqd-btn:hover { opacity: 0.85; }
14
+ .sqd-btn--primary { background: var(--primary); color: #fff; border-color: var(--primary); }
15
+ .sqd-btn--danger { background: var(--danger); color: #fff; border-color: var(--danger); }
16
+ .sqd-btn--muted { background: var(--surface); color: var(--text); border-color: var(--border); }
17
+ .sqd-btn--sm { padding: 0.2rem 0.55rem; font-size: 11px; }
18
+
19
+ .sqd-row-actions { white-space: nowrap; text-align: right; width: 1%; }
20
+ .sqd-row-actions form { display: inline; margin-left: 0.25rem; }
21
+
22
+ .sqd-selection-bar {
23
+ display: flex;
24
+ align-items: center;
25
+ gap: 0.75rem;
26
+ padding: 0.5rem 1rem;
27
+ background: var(--bg);
28
+ border-bottom: 1px solid var(--border);
29
+ font-size: 13px;
30
+ }
31
+
32
+ table th input[type="checkbox"],
33
+ table td input[type="checkbox"] {
34
+ width: 15px;
35
+ height: 15px;
36
+ cursor: pointer;
37
+ accent-color: var(--primary);
38
+ }
@@ -0,0 +1,103 @@
1
+ .sqd-search {
2
+ display: flex;
3
+ gap: 0.5rem;
4
+ align-items: center;
5
+ margin-bottom: 1rem;
6
+ }
7
+
8
+ .sqd-search__input {
9
+ width: 280px;
10
+ padding: 0.35rem 0.75rem;
11
+ border: 1px solid var(--border);
12
+ border-radius: 5px;
13
+ font-size: 13px;
14
+ background: var(--surface);
15
+ color: var(--text);
16
+ line-height: 1.5;
17
+ }
18
+
19
+ .sqd-search__input:focus {
20
+ outline: 2px solid var(--primary);
21
+ outline-offset: -1px;
22
+ border-color: var(--primary);
23
+ }
24
+
25
+ @media (max-width: 640px) {
26
+ .sqd-search { flex-wrap: wrap; }
27
+ .sqd-search__input { width: 100%; }
28
+ }
29
+
30
+ .sqd-search--global { margin-bottom: 2rem; }
31
+
32
+ .sqd-search__input--lg {
33
+ width: 420px;
34
+ font-size: 15px;
35
+ padding: 0.5rem 1rem;
36
+ }
37
+
38
+ @media (max-width: 640px) {
39
+ .sqd-search__input--lg { width: 100%; }
40
+ }
41
+
42
+ .sqd-search-group {
43
+ margin-bottom: 2rem;
44
+ }
45
+
46
+ .sqd-search-group__header {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 0.75rem;
50
+ margin-bottom: 0.75rem;
51
+ }
52
+
53
+ .sqd-filters {
54
+ display: flex;
55
+ gap: 0.5rem;
56
+ flex-wrap: wrap;
57
+ margin-bottom: 1rem;
58
+ }
59
+
60
+ .sqd-filters a {
61
+ padding: 0.35rem 0.875rem;
62
+ border-radius: 20px;
63
+ font-size: 12px;
64
+ font-weight: 500;
65
+ text-decoration: none;
66
+ border: 1px solid var(--border);
67
+ color: var(--muted);
68
+ background: var(--surface);
69
+ transition: all 0.1s;
70
+ }
71
+
72
+ .sqd-filters a:hover,
73
+ .sqd-filters a.active {
74
+ background: var(--primary);
75
+ border-color: var(--primary);
76
+ color: #fff;
77
+ }
78
+
79
+ .sqd-period-filter {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 0.25rem;
83
+ margin-left: auto;
84
+ }
85
+
86
+ .sqd-period-filter a {
87
+ padding: 0.2rem 0.55rem;
88
+ border-radius: 4px;
89
+ font-size: 11px;
90
+ font-weight: 500;
91
+ text-decoration: none;
92
+ border: 1px solid var(--border);
93
+ color: var(--muted);
94
+ background: var(--surface);
95
+ transition: all 0.1s;
96
+ }
97
+
98
+ .sqd-period-filter a:hover,
99
+ .sqd-period-filter a.active {
100
+ background: var(--muted);
101
+ border-color: var(--muted);
102
+ color: #fff;
103
+ }
@@ -0,0 +1,84 @@
1
+ .sqd-mono {
2
+ font-family: "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
3
+ font-size: 12px;
4
+ }
5
+
6
+ .sqd-error-msg {
7
+ color: var(--danger);
8
+ font-size: 12px;
9
+ }
10
+
11
+ .sqd-truncate {
12
+ max-width: 320px;
13
+ overflow: hidden;
14
+ text-overflow: ellipsis;
15
+ white-space: nowrap;
16
+ }
17
+
18
+ .sqd-detail-grid {
19
+ display: grid;
20
+ grid-template-columns: 1fr 1fr;
21
+ gap: 1.5rem;
22
+ }
23
+
24
+ .sqd-grid-2 {
25
+ display: grid;
26
+ grid-template-columns: 1fr 1fr;
27
+ gap: 1rem;
28
+ }
29
+
30
+ .sqd-breadcrumb {
31
+ font-size: 12px;
32
+ color: var(--muted);
33
+ margin-bottom: 0.25rem;
34
+ }
35
+
36
+ .sqd-breadcrumb a { color: var(--muted); text-decoration: none; }
37
+ .sqd-breadcrumb a:hover { color: var(--text); }
38
+
39
+ .sqd-detail-section { padding: 1.25rem; }
40
+
41
+ .sqd-section-title {
42
+ font-size: 13px;
43
+ font-weight: 600;
44
+ text-transform: uppercase;
45
+ letter-spacing: 0.05em;
46
+ color: var(--muted);
47
+ margin-bottom: 1rem;
48
+ }
49
+
50
+ .sqd-section-title--danger { color: var(--danger); }
51
+
52
+ .sqd-dl {
53
+ display: grid;
54
+ grid-template-columns: auto 1fr;
55
+ gap: 0.5rem 1.5rem;
56
+ font-size: 13px;
57
+ }
58
+
59
+ .sqd-dl dt { color: var(--muted); white-space: nowrap; }
60
+ .sqd-dl dd { word-break: break-all; }
61
+
62
+ .sqd-pre {
63
+ font-family: monospace;
64
+ font-size: 12px;
65
+ background: var(--bg);
66
+ border: 1px solid var(--border);
67
+ border-radius: 5px;
68
+ padding: 0.75rem;
69
+ overflow-x: auto;
70
+ white-space: pre-wrap;
71
+ word-break: break-word;
72
+ max-height: 400px;
73
+ overflow-y: auto;
74
+ }
75
+
76
+ .sqd-pre--muted { color: var(--muted); }
77
+
78
+ .sqd-error-header {
79
+ font-size: 13px;
80
+ padding: 0.5rem 0.75rem;
81
+ background: #f8d7da;
82
+ color: #842029;
83
+ border-radius: 5px;
84
+ }
@@ -0,0 +1,27 @@
1
+ nav.pagy {
2
+ display: flex;
3
+ justify-content: center;
4
+ gap: 0.25rem;
5
+ padding: 1rem;
6
+ list-style: none;
7
+ }
8
+
9
+ nav.pagy a {
10
+ display: inline-flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ min-width: 32px;
14
+ height: 32px;
15
+ padding: 0 0.5rem;
16
+ border-radius: 5px;
17
+ font-size: 13px;
18
+ text-decoration: none;
19
+ border: 1px solid var(--border);
20
+ color: var(--text);
21
+ background: var(--surface);
22
+ }
23
+
24
+ nav.pagy a:hover:not([aria-disabled="true"]) { background: var(--bg); }
25
+ nav.pagy a[aria-current="page"] { background: var(--primary); color: #fff; border-color: var(--primary); }
26
+ nav.pagy a[role="separator"],
27
+ nav.pagy a[aria-disabled="true"]:not([aria-current="page"]) { color: var(--muted); cursor: default; }