ideasbugs 0.7.2 → 0.7.3

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: 3508a2696b1f36bb9fd1e0c9e6a77cebb997d4db3652c81b508e1d8b3026b2fb
4
- data.tar.gz: 58cfb0e99d454d7fc576eef7caa7a737632180a48ce4cb7ecf0a42653c640e9b
3
+ metadata.gz: ce8bef2a9684476bf4fa0bfce5e9fed4169b9f3393c06fc75c7a21ce15f051f4
4
+ data.tar.gz: c5c4a9b8988d59da6bbb4f66e0e78d9cbb9f21e37c3304ee64061caf00613786
5
5
  SHA512:
6
- metadata.gz: d8638b0c249a286479b8fe6321b4d9c819aa88b87eab90303e17f0c0372653b5fc90da144f9adf2da62746e9c35f7ec52406b5d05df1e88cb6f7d1208b9198dd
7
- data.tar.gz: eb8692726633fe4c3d5b52f01e52d95efd700419bbac13b3672ef90dce483ecddcaae785ef2f3010c0f09879dc7fa28a9eeffe5906f2050a2e8529989dfcd34f
6
+ metadata.gz: bc31e9233dd59dd9799bb4bf40c28ccfa7cb8f78d31ebc655ba68f12c9c1727c9f7bad7990d7572e9d042ba7e077cfaf0f8cb7f0eeeda51d8973f5f17df1c969
7
+ data.tar.gz: 7375020ab5200629c5f7b65ebc6f5fe9941ab2e83f667aa0bb58fedefab4c6538a6d4cdac4ae94c2379db5f6d566cc407a23b695bc458add4d8f7fbe26ff5fb5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.3 (2026-07-31)
4
+
5
+ - Added `mount_ideasbugs at: "/feedback"` as the install-time route helper,
6
+ keeping `config.mount_path` synchronized with the mounted engine path while
7
+ preserving manual `mount Ideasbugs::Engine` compatibility.
8
+ - Extracted the dashboard stylesheet into a same-origin, fingerprinted
9
+ `/dashboard.css` endpoint and added CSP meta tags to the dashboard layout.
10
+ The public widget remains pipeline-free and controller-served.
11
+ - New `config.storage_service`: store screenshots on a named Active Storage
12
+ service from the host's `config/storage.yml` instead of the environment
13
+ default. Point it at a dedicated bucket, folder, or provider-specific service
14
+ entry to keep feedback screenshots separate from the rest of the app's media.
15
+ `nil` keeps today's behavior.
16
+
3
17
  ## 0.7.2 (2026-07-30)
4
18
 
5
19
  - Shipped the `IdeasBugs` admin dashboard title key in every bundled locale,
data/README.md CHANGED
@@ -96,6 +96,7 @@ Everything is optional — a fresh install works with zero config. In
96
96
  | `screenshots` | `true` | Allow uploads (needs Active Storage) |
97
97
  | `max_screenshots` | `3` | Enforced server-side |
98
98
  | `max_screenshot_size` | `5.megabytes` | Enforced server-side |
99
+ | `storage_service` | app default | Active Storage service for screenshots (`storage.yml` key) |
99
100
  | `rate_limit` | `{ to: 10, within: 1.minute }` | Per-IP throttle (Rails 7.2+). `nil` disables |
100
101
  | `show_button` | `true` | The floating button. `false` to use your own trigger |
101
102
  | `button_label` | `nil` | Fixed button text. `nil` uses the localized default |
@@ -7,7 +7,7 @@ module Ideasbugs
7
7
  # Drive swaps the <body> and re-runs body scripts under the *original*
8
8
  # page's CSP header, where a freshly minted inline nonce would be refused.
9
9
  class WidgetsController < ApplicationController
10
- # The script is static and carries no user data; without this, Rails'
10
+ # These assets are static and carry no user data; without this, Rails'
11
11
  # cross-origin JavaScript guard refuses to serve it to a plain
12
12
  # <script src> request.
13
13
  skip_forgery_protection
@@ -17,10 +17,20 @@ module Ideasbugs
17
17
  # fingerprinted URL is immutable and gets long-lived caching; anything
18
18
  # else only ETag-revalidates.
19
19
  def show
20
- expires_in 1.year, public: true if params[:v] == Widget.fingerprint
21
- return unless stale?(etag: [Ideasbugs::VERSION, Widget.fingerprint])
20
+ serve(Widget.javascript, Widget.fingerprint, 'text/javascript')
21
+ end
22
+
23
+ def dashboard_stylesheet
24
+ serve(Widget.dashboard_stylesheet, Widget.dashboard_stylesheet_fingerprint, 'text/css')
25
+ end
26
+
27
+ private
28
+
29
+ def serve(source, fingerprint, content_type)
30
+ expires_in 1.year, public: true if params[:v] == fingerprint
31
+ return unless stale?(etag: [Ideasbugs::VERSION, fingerprint])
22
32
 
23
- render plain: Widget.javascript, content_type: 'text/javascript'
33
+ render plain: source, content_type: content_type
24
34
  end
25
35
  end
26
36
  end
@@ -9,7 +9,11 @@ module Ideasbugs
9
9
  # as a scope name, and three statuses don't need the machinery anyway.
10
10
  STATUSES = %w[open in_review resolved].freeze
11
11
 
12
- has_many_attached :screenshots if defined?(::ActiveStorage)
12
+ if defined?(::ActiveStorage)
13
+ # Read at class load, after the host's initializer has run. nil falls
14
+ # through to Active Storage's environment default service.
15
+ has_many_attached :screenshots, service: Ideasbugs.config.storage_service
16
+ end
13
17
 
14
18
  validates :message, presence: true
15
19
  validates :status, inclusion: { in: STATUSES }
@@ -4,69 +4,9 @@
4
4
  <title><%= t('ideasbugs.dashboard.title', default: 'IdeasBugs') %></title>
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <%= csrf_meta_tags %>
7
- <style>
8
- :root {
9
- color-scheme: light dark;
10
- --bg: #f6f7f9; --surface: #fff; --text: #1c2024; --muted: #6b7280;
11
- --border: #e5e7eb; --accent: #2563eb; --accent-text: #fff;
12
- --bug: #dc2626; --feature: #16a34a; --other: #6b7280;
13
- --open: #2563eb; --in_review: #d97706; --resolved: #16a34a;
14
- }
15
- @media (prefers-color-scheme: dark) {
16
- :root {
17
- --bg: #111418; --surface: #1a1f26; --text: #e6e8ea; --muted: #9aa2ab;
18
- --border: #2a313a; --accent: #3b82f6;
19
- }
20
- }
21
- * { box-sizing: border-box; }
22
- body { margin: 0; background: var(--bg); color: var(--text);
23
- font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
24
- a { color: var(--accent); text-decoration: none; }
25
- a:hover { text-decoration: underline; }
26
- .container { max-width: 960px; margin: 0 auto; padding: 24px 16px 64px; }
27
- h1 { font-size: 22px; margin: 0 0 16px; }
28
- .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 16px; flex-wrap: wrap; }
29
- .tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--muted); }
30
- .tabs a.active { color: var(--text); font-weight: 600; border: 1px solid var(--border);
31
- border-bottom: 2px solid var(--surface); background: var(--surface); margin-bottom: -1px; }
32
- .tabs a:hover { text-decoration: none; color: var(--text); }
33
- .count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px;
34
- background: var(--border); font-size: 12px; text-align: center; }
35
- .filters { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; }
36
- .filters select, .filters input[type=search] { padding: 6px 8px; border: 1px solid var(--border);
37
- border-radius: 8px; background: var(--surface); color: var(--text); font: inherit; }
38
- .filters input[type=search] { flex: 1; max-width: 320px; }
39
- .card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
40
- table { width: 100%; border-collapse: collapse; }
41
- th, td { padding: 10px 14px; text-align: left; vertical-align: top; }
42
- th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
43
- border-bottom: 1px solid var(--border); }
44
- tr + tr td { border-top: 1px solid var(--border); }
45
- .badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600;
46
- color: #fff; white-space: nowrap; }
47
- .badge.kind-bug { background: var(--bug); }
48
- .badge.kind-feature { background: var(--feature); }
49
- .badge.kind-other, .badge.kind-default { background: var(--other); }
50
- .badge.status-open { background: var(--open); }
51
- .badge.status-in_review { background: var(--in_review); }
52
- .badge.status-resolved { background: var(--resolved); }
53
- .muted { color: var(--muted); font-size: 13px; }
54
- .pager { margin-top: 16px; display: flex; gap: 16px; }
55
- .actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 16px 0; }
56
- .actions form { display: inline; }
57
- button, .button { padding: 8px 14px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
58
- background: var(--surface); color: var(--text); font: inherit; }
59
- button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
60
- button.danger { color: var(--bug); }
61
- .message { white-space: pre-wrap; overflow-wrap: anywhere; }
62
- dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
63
- dt { color: var(--muted); }
64
- dd { margin: 0; overflow-wrap: anywhere; }
65
- .shots { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 8px; }
66
- .shots img { max-width: 280px; max-height: 200px; border: 1px solid var(--border); border-radius: 8px; }
67
- .empty { padding: 48px 16px; text-align: center; color: var(--muted); }
68
- .breadcrumb { margin-bottom: 12px; font-size: 13px; }
69
- </style>
7
+ <%= csp_meta_tag %>
8
+ <%= stylesheet_link_tag "#{dashboard_stylesheet_path}?v=#{Ideasbugs::Widget.dashboard_stylesheet_fingerprint}",
9
+ 'data-turbo-track': 'reload' %>
70
10
  </head>
71
11
  <body>
72
12
  <div class="container">
data/config/routes.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  Ideasbugs::Engine.routes.draw do
4
4
  # The widget code, served same-origin (see WidgetsController for why).
5
5
  get 'widget.js', to: 'widgets#show', as: :widget
6
+ get 'dashboard.css', to: 'widgets#dashboard_stylesheet', as: :dashboard_stylesheet
6
7
 
7
8
  # create is the public widget endpoint; the rest is the triage dashboard.
8
9
  resources :feedbacks, only: %i[create index show update destroy] do
@@ -22,7 +22,7 @@ module Ideasbugs
22
22
  end
23
23
 
24
24
  def mount_engine
25
- route %(mount Ideasbugs::Engine => "/feedback")
25
+ route %(mount_ideasbugs at: "/feedback")
26
26
  end
27
27
 
28
28
  def post_install
@@ -47,6 +47,11 @@ Ideasbugs.configure do |config|
47
47
  # config.screenshots = true
48
48
  # config.max_screenshots = 3
49
49
  # config.max_screenshot_size = 5.megabytes
50
+ #
51
+ # Store screenshots on a specific Active Storage service from
52
+ # config/storage.yml, such as a dedicated bucket or folder. Default: your
53
+ # environment's default service.
54
+ # config.storage_service = :ideasbugs_uploads
50
55
 
51
56
  # Per-IP throttle for the submission endpoint (Rails 7.2+; ignored on 7.1).
52
57
  # Keyword arguments for Rails' rate limiter; nil disables throttling.
@@ -59,7 +64,8 @@ Ideasbugs.configure do |config|
59
64
  # Fixed button text; leave nil to use the localized default.
60
65
  # config.button_label = nil
61
66
 
62
- # Keep in sync with the `mount` in config/routes.rb.
67
+ # Default mount path used by `mount_ideasbugs`.
68
+ # Override only if you mount the engine manually.
63
69
  # config.mount_path = "/feedback"
64
70
 
65
71
  # Called with each saved feedback — notify Slack, send an email, etc.
@@ -49,6 +49,10 @@ module Ideasbugs
49
49
  # Upload limits, enforced server-side and mirrored in the widget.
50
50
  attr_accessor :max_screenshots, :max_screenshot_size
51
51
 
52
+ # The Active Storage service that stores screenshots, as a service name
53
+ # from the host's config/storage.yml. nil uses the environment default.
54
+ attr_accessor :storage_service
55
+
52
56
  # Per-IP throttle for the public submission endpoint, as keyword arguments
53
57
  # for Rails' rate limiter (Rails 7.2+; ignored on 7.1). Read once when the
54
58
  # controller loads — set it in an initializer. nil disables throttling.
@@ -83,6 +87,7 @@ module Ideasbugs
83
87
  @screenshots = true
84
88
  @max_screenshots = 3
85
89
  @max_screenshot_size = 5 * 1024 * 1024
90
+ @storage_service = nil
86
91
  @rate_limit = { to: 10, within: 60 }
87
92
  @show_button = true
88
93
  @button_label = nil
@@ -0,0 +1,61 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ --bg: #f6f7f9; --surface: #fff; --text: #1c2024; --muted: #6b7280;
4
+ --border: #e5e7eb; --accent: #2563eb; --accent-text: #fff;
5
+ --bug: #dc2626; --feature: #16a34a; --other: #6b7280;
6
+ --open: #2563eb; --in_review: #d97706; --resolved: #16a34a;
7
+ }
8
+ @media (prefers-color-scheme: dark) {
9
+ :root {
10
+ --bg: #111418; --surface: #1a1f26; --text: #e6e8ea; --muted: #9aa2ab;
11
+ --border: #2a313a; --accent: #3b82f6;
12
+ }
13
+ }
14
+ * { box-sizing: border-box; }
15
+ body { margin: 0; background: var(--bg); color: var(--text);
16
+ font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
17
+ a { color: var(--accent); text-decoration: none; }
18
+ a:hover { text-decoration: underline; }
19
+ .container { max-width: 960px; margin: 0 auto; padding: 24px 16px 64px; }
20
+ h1 { font-size: 22px; margin: 0 0 16px; }
21
+ .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 16px; flex-wrap: wrap; }
22
+ .tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--muted); }
23
+ .tabs a.active { color: var(--text); font-weight: 600; border: 1px solid var(--border);
24
+ border-bottom: 2px solid var(--surface); background: var(--surface); margin-bottom: -1px; }
25
+ .tabs a:hover { text-decoration: none; color: var(--text); }
26
+ .count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px;
27
+ background: var(--border); font-size: 12px; text-align: center; }
28
+ .filters { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; }
29
+ .filters select, .filters input[type=search] { padding: 6px 8px; border: 1px solid var(--border);
30
+ border-radius: 8px; background: var(--surface); color: var(--text); font: inherit; }
31
+ .filters input[type=search] { flex: 1; max-width: 320px; }
32
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
33
+ table { width: 100%; border-collapse: collapse; }
34
+ th, td { padding: 10px 14px; text-align: left; vertical-align: top; }
35
+ th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
36
+ border-bottom: 1px solid var(--border); }
37
+ tr + tr td { border-top: 1px solid var(--border); }
38
+ .badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600;
39
+ color: #fff; white-space: nowrap; }
40
+ .badge.kind-bug { background: var(--bug); }
41
+ .badge.kind-feature { background: var(--feature); }
42
+ .badge.kind-other, .badge.kind-default { background: var(--other); }
43
+ .badge.status-open { background: var(--open); }
44
+ .badge.status-in_review { background: var(--in_review); }
45
+ .badge.status-resolved { background: var(--resolved); }
46
+ .muted { color: var(--muted); font-size: 13px; }
47
+ .pager { margin-top: 16px; display: flex; gap: 16px; }
48
+ .actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 16px 0; }
49
+ .actions form { display: inline; }
50
+ button, .button { padding: 8px 14px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
51
+ background: var(--surface); color: var(--text); font: inherit; }
52
+ button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
53
+ button.danger { color: var(--bug); }
54
+ .message { white-space: pre-wrap; overflow-wrap: anywhere; }
55
+ dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
56
+ dt { color: var(--muted); }
57
+ dd { margin: 0; overflow-wrap: anywhere; }
58
+ .shots { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 8px; }
59
+ .shots img { max-width: 280px; max-height: 200px; border: 1px solid var(--border); border-radius: 8px; }
60
+ .empty { padding: 48px 16px; text-align: center; color: var(--muted); }
61
+ .breadcrumb { margin-bottom: 12px; font-size: 13px; }
@@ -17,5 +17,14 @@ module Ideasbugs
17
17
  extend Ideasbugs::HasFeedback
18
18
  end
19
19
  end
20
+
21
+ initializer 'ideasbugs.routing' do
22
+ ActionDispatch::Routing::Mapper.include(Module.new do
23
+ def mount_ideasbugs(at: Ideasbugs.config.mount_path, **options)
24
+ Ideasbugs.config.mount_path = at
25
+ mount Ideasbugs::Engine, at:, **options
26
+ end
27
+ end)
28
+ end
20
29
  end
21
30
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ideasbugs
4
- VERSION = '0.7.2'
4
+ VERSION = '0.7.3'
5
5
  end
@@ -13,6 +13,7 @@ module Ideasbugs
13
13
  # host's asset namespace or precompiled output.
14
14
  module Widget
15
15
  SOURCE = File.expand_path('widget.js', __dir__)
16
+ DASHBOARD_STYLESHEET_SOURCE = File.expand_path('dashboard.css', __dir__)
16
17
 
17
18
  # Right-to-left scripts, so the form renders mirrored for those locales.
18
19
  # Matched on the language subtag, so region variants ("ar-EG") count too.
@@ -23,6 +24,10 @@ module Ideasbugs
23
24
  @javascript ||= File.read(SOURCE)
24
25
  end
25
26
 
27
+ def dashboard_stylesheet
28
+ @dashboard_stylesheet ||= File.read(DASHBOARD_STYLESHEET_SOURCE)
29
+ end
30
+
26
31
  # Content fingerprint for the cache-busting script URL: a changed file
27
32
  # is a changed URL, so no browser can ever run stale widget code —
28
33
  # Safari has been caught ignoring must-revalidate on same-URL scripts.
@@ -30,6 +35,10 @@ module Ideasbugs
30
35
  @fingerprint ||= Digest::MD5.hexdigest(javascript)
31
36
  end
32
37
 
38
+ def dashboard_stylesheet_fingerprint
39
+ @dashboard_stylesheet_fingerprint ||= Digest::MD5.hexdigest(dashboard_stylesheet)
40
+ end
41
+
33
42
  # The two <script> tags the helper renders.
34
43
  #
35
44
  # The config rides in a `type="application/json"` block: it is *data*,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ideasbugs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov
@@ -82,6 +82,7 @@ files:
82
82
  - lib/generators/ideasbugs/tenant/tenant_generator.rb
83
83
  - lib/ideasbugs.rb
84
84
  - lib/ideasbugs/configuration.rb
85
+ - lib/ideasbugs/dashboard.css
85
86
  - lib/ideasbugs/engine.rb
86
87
  - lib/ideasbugs/has_feedback.rb
87
88
  - lib/ideasbugs/version.rb