feedback_engine 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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +27 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +265 -0
  5. data/Rakefile +8 -0
  6. data/app/assets/feedback_engine/widget.js +368 -0
  7. data/app/controllers/feedback_engine/application_controller.rb +15 -0
  8. data/app/controllers/feedback_engine/feedbacks_controller.rb +120 -0
  9. data/app/helpers/feedback_engine/widget_helper.rb +18 -0
  10. data/app/models/feedback_engine/application_record.rb +7 -0
  11. data/app/models/feedback_engine/feedback.rb +30 -0
  12. data/app/views/feedback_engine/feedbacks/index.html.erb +75 -0
  13. data/app/views/feedback_engine/feedbacks/show.html.erb +63 -0
  14. data/app/views/layouts/feedback_engine/application.html.erb +75 -0
  15. data/config/locales/feedback_engine.ar.yml +23 -0
  16. data/config/locales/feedback_engine.bg.yml +23 -0
  17. data/config/locales/feedback_engine.bn.yml +23 -0
  18. data/config/locales/feedback_engine.de.yml +23 -0
  19. data/config/locales/feedback_engine.el.yml +23 -0
  20. data/config/locales/feedback_engine.en.yml +43 -0
  21. data/config/locales/feedback_engine.es.yml +23 -0
  22. data/config/locales/feedback_engine.fr.yml +23 -0
  23. data/config/locales/feedback_engine.hi.yml +23 -0
  24. data/config/locales/feedback_engine.hr.yml +23 -0
  25. data/config/locales/feedback_engine.id.yml +23 -0
  26. data/config/locales/feedback_engine.it.yml +23 -0
  27. data/config/locales/feedback_engine.ja.yml +23 -0
  28. data/config/locales/feedback_engine.ko.yml +23 -0
  29. data/config/locales/feedback_engine.lb.yml +23 -0
  30. data/config/locales/feedback_engine.nl.yml +23 -0
  31. data/config/locales/feedback_engine.pl.yml +23 -0
  32. data/config/locales/feedback_engine.pt.yml +23 -0
  33. data/config/locales/feedback_engine.ro.yml +23 -0
  34. data/config/locales/feedback_engine.ru.yml +23 -0
  35. data/config/locales/feedback_engine.th.yml +23 -0
  36. data/config/locales/feedback_engine.tr.yml +23 -0
  37. data/config/locales/feedback_engine.uk.yml +23 -0
  38. data/config/locales/feedback_engine.ur.yml +23 -0
  39. data/config/locales/feedback_engine.vi.yml +23 -0
  40. data/config/locales/feedback_engine.zh-CN.yml +23 -0
  41. data/config/routes.rb +8 -0
  42. data/lib/feedback_engine/configuration.rb +87 -0
  43. data/lib/feedback_engine/engine.rb +13 -0
  44. data/lib/feedback_engine/version.rb +5 -0
  45. data/lib/feedback_engine/widget.rb +107 -0
  46. data/lib/feedback_engine.rb +34 -0
  47. data/lib/generators/feedback_engine/install/install_generator.rb +41 -0
  48. data/lib/generators/feedback_engine/install/templates/create_feedback_engine_feedbacks.rb.tt +21 -0
  49. data/lib/generators/feedback_engine/install/templates/initializer.rb +43 -0
  50. metadata +110 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: df04e8fe3b41eb7019841bb7badfc9b55702401e84c8a43fd53eb979f130c629
4
+ data.tar.gz: '00149e51b454535b3d4ffb6441bfc2c20c55475f50d2967b25be7070bed57fd4'
5
+ SHA512:
6
+ metadata.gz: 3b7eed3a13ee10859d286f34feada2b28490b3a8a658c9ee2ab4fd479c69ab4481333ea16c208494f5ee6cd789656baf18b0612d4656267020849acf37d330b0
7
+ data.tar.gz: 18291f5570d23ad44ce193ef0170e30da2b5c594b64af9f04a0ffd764bc6c18026fa3db4c4000d58f2007c1621cc2407b6362391d798a437658b93d8749f415e
data/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-07-21)
4
+
5
+ Initial release.
6
+
7
+ - Drop-in feedback widget (`<%= feedback_engine_tag %>`): floating button +
8
+ self-styled modal with type, optional section, message, and optional
9
+ screenshots. Plain JavaScript, no build step, CSP-nonce aware, Turbo-safe,
10
+ follows system light/dark appearance and the app's `I18n.locale` (RTL
11
+ supported).
12
+ - `feedback_engine_feedbacks` table with kind, section, message, status
13
+ (open / in_review / resolved), page URL, user agent, and loose author
14
+ attribution.
15
+ - Screenshot uploads via Active Storage with server-side count / size /
16
+ content-type validation (configurable limits).
17
+ - Built-in triage dashboard at the mount path: status tabs with counts, type
18
+ filter, detail view with screenshots, status transitions, delete. Gated by
19
+ `config.authorize_admin` (development-only by default).
20
+ - Configuration hooks: `enabled`, `authorize_admin`, `current_user`,
21
+ `author_label`, `kinds`, `sections`, `screenshots` limits, `show_button`,
22
+ `button_label`, `mount_path`, `on_submit`.
23
+ - `feedback_engine:install` generator (initializer, migration, mount).
24
+ - Widget translations for English plus 25 more languages (ar, bg, bn, de, el,
25
+ es, fr, hi, hr, id, it, ja, ko, lb, nl, pl, pt, ro, ru, th, tr, uk, ur, vi,
26
+ zh-CN), with a parity spec keeping every locale's key set and interpolation
27
+ placeholders in sync.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2026 Yaroslav Shmarov
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,265 @@
1
+ # feedback_engine
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/feedback_engine)](https://rubygems.org/gems/feedback_engine)
4
+ [![CI](https://github.com/yshmarov/feedback-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/yshmarov/feedback-engine/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](MIT-LICENSE)
6
+
7
+ In-app product feedback collection for Rails.
8
+
9
+ `feedback_engine` adds a **"Send feedback"** widget to your app — bug reports,
10
+ feature requests, general comments, with optional screenshots — and stores
11
+ every submission in your own database. A minimal built-in dashboard lets you
12
+ browse and triage what users send. No third-party service, no data leaves your
13
+ app.
14
+
15
+ - **Zero UI dependencies.** The widget is plain JavaScript and styles itself.
16
+ No Tailwind, no Stimulus, no importmap, no build step. The dashboard renders
17
+ its own styles too.
18
+ - **One line in your layout.** `<%= feedback_engine_tag %>` and you're
19
+ collecting feedback.
20
+ - **Trigger it your way.** Use the built-in floating button, or hide it and
21
+ open the form from any element with a `data-feedback-engine-open` attribute.
22
+ - **Screenshots included.** Users can attach up to 3 images (via Active
23
+ Storage) — limits are configurable and enforced server-side.
24
+ - **Pluggable gating and attribution.** You decide who can send feedback, who
25
+ can read it, and how a submission is attributed to a user.
26
+ - **Localized.** The widget follows your app's `I18n.locale`; translations ship
27
+ for English plus 25 more languages — Arabic, Bengali, Bulgarian, Chinese
28
+ (Simplified), Croatian, Dutch, French, German, Greek, Hindi, Indonesian,
29
+ Italian, Japanese, Korean, Luxembourgish, Polish, Portuguese, Romanian,
30
+ Russian, Spanish, Thai, Turkish, Ukrainian, Urdu and Vietnamese — with
31
+ English fallbacks for everything else. RTL locales render mirrored.
32
+
33
+ ## How it works
34
+
35
+ 1. `feedback_engine_tag` renders a floating **Feedback** button (bottom-right).
36
+ 2. Clicking it opens a small self-styled modal: type (bug / feature / other),
37
+ an optional section select, a message, and optional screenshots.
38
+ 3. The form `POST`s to the mounted engine with the page URL, the user agent,
39
+ and (if you configure it) the current user — stored in the
40
+ `feedback_engine_feedbacks` table.
41
+ 4. You browse and triage submissions at the mount path (`/feedback`): status
42
+ tabs (open → in review → resolved), type filter, screenshots inline.
43
+
44
+ ## Requirements
45
+
46
+ - Ruby >= 3.2
47
+ - Rails >= 7.1
48
+ - Active Storage (only if you want screenshot uploads)
49
+
50
+ ## Installation
51
+
52
+ ```ruby
53
+ # Gemfile
54
+ gem "feedback_engine"
55
+ ```
56
+
57
+ ```bash
58
+ bundle install
59
+ bin/rails generate feedback_engine:install
60
+ bin/rails db:migrate
61
+ ```
62
+
63
+ The generator:
64
+
65
+ - writes `config/initializers/feedback_engine.rb`,
66
+ - creates the `feedback_engine_feedbacks` migration,
67
+ - mounts the engine in `config/routes.rb`:
68
+
69
+ ```ruby
70
+ mount FeedbackEngine::Engine => "/feedback"
71
+ ```
72
+
73
+ Then add the widget to your layout, right before `</body>`:
74
+
75
+ ```erb
76
+ <%= feedback_engine_tag %>
77
+ ```
78
+
79
+ > The widget reads the CSRF token from `<meta name="csrf-token">`, which
80
+ > `csrf_meta_tags` in your layout already provides in a standard Rails app.
81
+
82
+ Boot the app and look for the **Feedback** button in the bottom-right corner.
83
+ Submissions appear at [`/feedback`](http://localhost:3000/feedback) (dashboard
84
+ access defaults to development only — see below).
85
+
86
+ ## Configuration
87
+
88
+ Everything is optional; the defaults work out of the box.
89
+
90
+ ```ruby
91
+ # config/initializers/feedback_engine.rb
92
+ FeedbackEngine.configure do |config|
93
+ # Who can send feedback. Return false to hide the widget and reject
94
+ # submissions for this request. Defaults to everyone.
95
+ config.enabled = ->(request) { true }
96
+
97
+ # Who can browse and triage feedback at the mount path.
98
+ # DEFAULTS TO DEVELOPMENT ONLY — override before deploying.
99
+ config.authorize_admin = ->(request) { request.env["warden"]&.user&.admin? }
100
+
101
+ # Attribute feedback to a user (optional). Return an object responding to
102
+ # #id, or nil. Receives the request.
103
+ config.current_user = ->(request) { request.env["warden"]&.user }
104
+
105
+ # Label stored for the author and shown in the dashboard.
106
+ config.author_label = ->(user) { user.try(:email) }
107
+
108
+ # Feedback types users can pick from. Labels come from I18n
109
+ # (feedback_engine.kinds.<kind>).
110
+ config.kinds = %w[bug feature other]
111
+
112
+ # App areas shown as a select in the widget. Empty list hides the select.
113
+ config.sections = ["Dashboard", "Billing", "Settings"]
114
+
115
+ # Screenshot uploads (requires Active Storage).
116
+ config.screenshots = true
117
+ config.max_screenshots = 3
118
+ config.max_screenshot_size = 5.megabytes
119
+
120
+ # Show the floating feedback button. Set false and trigger the form from
121
+ # your own UI instead (see below).
122
+ config.show_button = true
123
+
124
+ # Fixed button text; leave nil to use the localized default.
125
+ config.button_label = nil
126
+
127
+ # Keep in sync with the `mount` in config/routes.rb.
128
+ config.mount_path = "/feedback"
129
+
130
+ # Called with each saved feedback — notify Slack, send an email, etc.
131
+ config.on_submit = ->(feedback) { FeedbackMailer.with(feedback:).new_feedback.deliver_later }
132
+ end
133
+ ```
134
+
135
+ ### Opening the form from your own UI
136
+
137
+ Prefer a nav item over the floating button? Add `data-feedback-engine-open` to
138
+ any element and (optionally) hide the button:
139
+
140
+ ```ruby
141
+ config.show_button = false # optional
142
+ ```
143
+
144
+ ```erb
145
+ <button data-feedback-engine-open>Send feedback</button>
146
+ ```
147
+
148
+ ### Protecting the dashboard
149
+
150
+ The dashboard (index/show/triage) is gated by `config.authorize_admin`, which
151
+ defaults to **development only** so a fresh install can never leak feedback in
152
+ production. Grant access however your app resolves admins:
153
+
154
+ ```ruby
155
+ # Devise:
156
+ config.authorize_admin = ->(request) { request.env["warden"]&.user&.admin? }
157
+
158
+ # Basic auth, feature flag, IP allowlist — anything based on the request:
159
+ config.authorize_admin = ->(request) { Flipper.enabled?(:feedback_admin) }
160
+ ```
161
+
162
+ You can also wrap the mount in your own routing constraint (the lambda still
163
+ applies on top):
164
+
165
+ ```ruby
166
+ authenticate :user, ->(user) { user.admin? } do
167
+ mount FeedbackEngine::Engine => "/feedback"
168
+ end
169
+ ```
170
+
171
+ ### Screenshots
172
+
173
+ Uploads use Active Storage: `bin/rails active_storage:install` if you haven't
174
+ already. Limits (`max_screenshots`, `max_screenshot_size`) are enforced
175
+ server-side and shown as a hint in the widget. If Active Storage isn't loaded,
176
+ the upload control simply doesn't render and uploads are rejected.
177
+
178
+ ### Notifications
179
+
180
+ `config.on_submit` runs inline after each save — keep it fast or hand off to a
181
+ job:
182
+
183
+ ```ruby
184
+ config.on_submit = ->(feedback) do
185
+ SlackNotifier.post("New #{feedback.kind}: #{feedback.message.truncate(100)}")
186
+ end
187
+ ```
188
+
189
+ ### Localizing the widget
190
+
191
+ Every string resolves through Rails I18n under the `feedback_engine.*` scope
192
+ and follows the current `I18n.locale`. Missing keys fall back to English. To
193
+ add a language or reword the bundled copy, define the keys in your own locale
194
+ files (yours win over the gem's):
195
+
196
+ ```yaml
197
+ # config/locales/nl.yml
198
+ nl:
199
+ feedback_engine:
200
+ button: "Feedback"
201
+ title: "Feedback versturen"
202
+ kinds:
203
+ bug: "Fout melden"
204
+ feature: "Functie aanvragen"
205
+ other: "Overig"
206
+ # …see config/locales/feedback_engine.en.yml for the full key list
207
+ ```
208
+
209
+ Custom kinds get their labels the same way (`feedback_engine.kinds.<kind>`),
210
+ falling back to `kind.humanize`.
211
+
212
+ ### Light / dark / system appearance
213
+
214
+ Both the widget and the dashboard follow the operating-system appearance via
215
+ `prefers-color-scheme` — no configuration needed.
216
+
217
+ ## Working with feedback in code
218
+
219
+ Submissions are ordinary records:
220
+
221
+ ```ruby
222
+ FeedbackEngine::Feedback.where(status: "open").newest_first.each do |feedback|
223
+ puts "[#{feedback.kind}] #{feedback.message} — #{feedback.author_label}"
224
+ end
225
+ ```
226
+
227
+ Each row stores `kind`, `section`, `message`, `status` (`open`, `in_review`,
228
+ `resolved`), `page_url`, `user_agent`, and optional `author_id` /
229
+ `author_label`. Screenshots are Active Storage attachments
230
+ (`feedback.screenshots`).
231
+
232
+ ## Security
233
+
234
+ - Submission and dashboard access are both gated **on the server** for every
235
+ request; the dashboard denies everything outside development until you
236
+ configure `authorize_admin`.
237
+ - Screenshot count, size, and content type (images only) are validated
238
+ server-side, regardless of what the client claims.
239
+ - The widget code carries the request's Content-Security-Policy nonce (the
240
+ same one `ActionDispatch` emits), so it runs under a nonce-based
241
+ `script-src` policy with no configuration. The runtime config ships as a
242
+ `<script type="application/json">` block (data, not code), so it needs no
243
+ nonce and stays correct across Turbo visits.
244
+ - Author attribution is stored as loose fields (no foreign key into your user
245
+ table), so the gem never couples to your user model.
246
+
247
+ ## Turbo
248
+
249
+ Works with Turbo Drive out of the box. Turbo replaces `<body>` on every visit,
250
+ which would take the floating button with it, so the widget registers its
251
+ document-level listeners once and re-renders on `turbo:load`.
252
+
253
+ ## Development
254
+
255
+ ```bash
256
+ bin/setup # or: bundle install
257
+ bundle exec rspec
258
+ bundle exec rubocop
259
+ ```
260
+
261
+ Tests run against a dummy Rails app under `spec/dummy`.
262
+
263
+ ## License
264
+
265
+ Released under the [MIT License](MIT-LICENSE).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec