railwatch 0.2.0.pre1 → 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: 9500835353b9f428ff09ae908f9cbc360c44f0ea01740139738ec8c06ce73eb2
4
- data.tar.gz: 7b4e2b48a881ef09243af6d2f51865609bd5615e55d27d9fca784f3e2740de7b
3
+ metadata.gz: e971a5ebd07d60e1ce62861a43de0d35961d1f12e439fc07a8e384e5fe7cd032
4
+ data.tar.gz: e3f9c39e07cc99599587b910ef777d8ce473f917bd85238414d5cfc7cc3892c1
5
5
  SHA512:
6
- metadata.gz: 0d38ab1fced44ae3168de8f7864b858732ad07a26a4a9a96d82b74891d3b20144b9cc19a703743a127a2d270552ba422e9d51c1c9f8e768077c5a12364f613c8
7
- data.tar.gz: 0e4ad6b17f76856cc2f160c64c992ed8405e50ba9485563c6e3b40495fdb11c1da756d8aa0317adec1d78b93f4b3dc6783de196c33c42d58dbbc606e3491310f
6
+ metadata.gz: 1e5bed88aee6554e1eb97f7e109a08f67b96ca286c378c8bcaba0c4284ab2f9b8b047ea5ceae073ff3c0ac800ed0e0386ebc7b0b25f5a66f0b144d042be62d59
7
+ data.tar.gz: 1ef06458ea5d88cd739def97a3e05b2755b8b0ba9c8666be531cc1e6e986f774f7c2e963a91c9affb9815f2770bab5e2f1b643a5477b41e2b766ad831948a2b9
data/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.2.0 (2026-09-18)
4
4
 
5
5
  - Embedded mode: `bin/rails generate railwatch:install --local` keeps
6
6
  every record in two SQLite databases the app owns (`railwatch` for
@@ -166,6 +166,13 @@
166
166
  the bundle yet (a `LoadError` rather than `AdapterNotSpecified`), which
167
167
  is the state a PostgreSQL app is in between the installer adding
168
168
  `gem "sqlite3"` and the `bundle install` that follows.
169
+ - Host user ids are opaque. The columns behind comments, issue activity,
170
+ saved views and issue assignment were integers, which assumed every
171
+ application numbers its users: an app with UUID primary keys could not
172
+ write a comment at all, and one with ids past the signed range could
173
+ not either. They are strings now, a `dashboard_user` resolver may
174
+ return whatever shape the app already uses, and rows written as numbers
175
+ before the change still match the same person after it.
169
176
  - The embedded dashboard authenticates the way Mission Control Jobs
170
177
  does: HTTP Basic is on and closed by default, so with no credentials
171
178
  every dashboard page answers 401 (with a note saying what to run), the
@@ -37,7 +37,7 @@ module Railwatch
37
37
  # dashboard_user resolver returns one identity for everybody is unaffected
38
38
  # either way: one owner, one editor.
39
39
  def editable?(view)
40
- view.viewer_id == Viewer.user.id
40
+ view.viewer_id.to_s == Viewer.user.id.to_s
41
41
  end
42
42
 
43
43
  def fallback = application_environment_overview_path(application, environment)
@@ -11,7 +11,7 @@ module Railwatch
11
11
  end
12
12
 
13
13
  def user=(u)
14
- self.viewer_id = u&.id
14
+ self.viewer_id = u&.id&.to_s
15
15
  end
16
16
 
17
17
  validates :body, presence: true
@@ -22,7 +22,7 @@ module Railwatch
22
22
  end
23
23
 
24
24
  def assignee=(user)
25
- self.assignee_id = user&.id
25
+ self.assignee_id = user&.id&.to_s
26
26
  end
27
27
  belongs_to :merged_into, class_name: "Issue", optional: true
28
28
  has_many :merged_issues, class_name: "Issue", foreign_key: :merged_into_id, inverse_of: :merged_into, dependent: :nullify
@@ -15,7 +15,7 @@ module Railwatch
15
15
  end
16
16
 
17
17
  def user=(u)
18
- self.viewer_id = u&.id
18
+ self.viewer_id = u&.id&.to_s
19
19
  end
20
20
 
21
21
  validates :kind, inclusion: { in: KINDS }
@@ -33,14 +33,14 @@ module Railwatch
33
33
  end
34
34
 
35
35
  def user=(u)
36
- self.viewer_id = u&.id
36
+ self.viewer_id = u&.id&.to_s
37
37
  end
38
38
 
39
39
  validates :name, presence: true, length: { maximum: 80 }
40
40
  validates :page, inclusion: { in: PAGES }
41
41
 
42
42
  scope :pinned, -> { where(pinned: true) }
43
- scope :visible_to, ->(user) { where(shared: true).or(where(viewer_id: user.id)) }
43
+ scope :visible_to, ->(user) { where(shared: true).or(where(viewer_id: user.id.to_s)) }
44
44
 
45
45
  # The props every environment page shares, ready for the sidebar and the
46
46
  # Views menu: one entry per view this user is allowed to see.
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The ids on comments, issue activity, saved views and issue assignment are
4
+ # whatever the host's `dashboard_user` resolver returned. They were integer
5
+ # columns, which quietly assumed every application numbers its users. An app
6
+ # with UUID primary keys could not store a comment at all, and one with ids
7
+ # past the signed range could not either.
8
+ #
9
+ # Nothing joins on these columns and nothing indexes them; they are an opaque
10
+ # handle the host gave us and hands back. A string says that, and costs
11
+ # nothing in a database that is a few thousand rows of authored content.
12
+ #
13
+ # Existing values convert cleanly: SQLite rewrites 7 as "7", and the models
14
+ # compare with to_s on both sides, so a row written before this migration
15
+ # still matches the same viewer after it.
16
+ class WidenHostUserIds < ActiveRecord::Migration[8.1]
17
+ COLUMNS = {
18
+ railwatch_issues: :assignee_id,
19
+ railwatch_comments: :viewer_id,
20
+ railwatch_issue_activities: :viewer_id,
21
+ railwatch_saved_views: :viewer_id
22
+ }.freeze
23
+
24
+ # SQLite rebuilds a table to change a column type, and a rebuild inside a
25
+ # transaction ignores `PRAGMA foreign_keys = OFF`. This database declares no
26
+ # foreign keys, so there is nothing to cascade, but the rebuild is kept out
27
+ # of the migrator's transaction anyway: it is the cheap half of a habit
28
+ # whose expensive half is deleted rows.
29
+ disable_ddl_transaction!
30
+
31
+ def up
32
+ COLUMNS.each do |table, column|
33
+ null = column == :viewer_id && table == :railwatch_saved_views ? false : true
34
+ change_column table, column, :string, limit: 255, null: null
35
+ end
36
+ end
37
+
38
+ # Reversible only while every stored id still looks like a number. Once a
39
+ # host with UUIDs (or emails, or anything else) has written one, there is
40
+ # no integer to go back to: the column would either refuse the value or
41
+ # quietly coerce it to something that is no longer that person. Say so and
42
+ # stop, rather than losing the identity on the way down. These tables are
43
+ # authored content, a few thousand rows at most, so reading them is cheap.
44
+ INTEGERISH = /\A-?\d+\z/
45
+ MAX_REPORTED = 3
46
+
47
+ def down
48
+ blocking = COLUMNS.filter_map do |table, column|
49
+ next unless connection.table_exists?(table)
50
+
51
+ values = connection.select_values(
52
+ "SELECT DISTINCT #{connection.quote_column_name(column)} FROM #{connection.quote_table_name(table)} " \
53
+ "WHERE #{connection.quote_column_name(column)} IS NOT NULL"
54
+ )
55
+ offenders = values.reject { |value| INTEGERISH.match?(value.to_s) }
56
+ "#{table}.#{column} (#{offenders.first(MAX_REPORTED).join(', ')}#{"..." if offenders.size > MAX_REPORTED})" if offenders.any?
57
+ end
58
+
59
+ if blocking.any?
60
+ raise ActiveRecord::IrreversibleMigration,
61
+ "cannot narrow host user ids back to integers: #{blocking.join('; ')}. " \
62
+ "Those ids came from this application's own dashboard_user resolver and have no integer form; " \
63
+ "rolling back would discard them."
64
+ end
65
+
66
+ COLUMNS.each do |table, column|
67
+ null = column == :viewer_id && table == :railwatch_saved_views ? false : true
68
+ change_column table, column, :integer, null: null
69
+ end
70
+ end
71
+ end
data/docs/embedded.md CHANGED
@@ -184,7 +184,7 @@ on the WebSocket handshake, a `dashboard_user` resolver is consulted,
184
184
  an undeclared gate is refused. The channel carries an ingest ping (a
185
185
  timestamp and per-type counts) and never telemetry records.
186
186
 
187
- ### Naming the operator
187
+ ### Naming the operator, and what an id may be
188
188
 
189
189
  Comments, saved views and issue activity record who did them. Give the
190
190
  initializer a resolver and the dashboard shows that person instead of a
@@ -197,6 +197,16 @@ c.dashboard_user = ->(request) do
197
197
  end
198
198
  ```
199
199
 
200
+ The `id` may be anything your application already uses: an integer, a
201
+ UUID, a ULID, an email. It is stored as an opaque string and handed back
202
+ to you; nothing joins on it and nothing parses it, because the engine has
203
+ no user table to check it against. Comments, saved views, issue activity
204
+ and assignment all key off whatever you return, so a person keeps their
205
+ own views and their name on their own comments however you identify them.
206
+
207
+ Returning `nil` refuses the request, which is what makes this an
208
+ authorisation rule as well as a label.
209
+
200
210
  ## The writer process
201
211
 
202
212
  Puma forks one Railwatch writer from its master when `config/puma.rb`
@@ -34,7 +34,10 @@ module Railwatch
34
34
  def plan = "embedded"
35
35
  def retention_days = Railwatch.config.retention_days
36
36
  def auto_resolve_after_days = 14
37
- def users = [ User.default ]
37
+ # Whoever the host's resolver named for this request, not the anonymous
38
+ # placeholder: an embedded install has no member list, so the account's
39
+ # one "member" is the person looking at it.
40
+ def users = [ User.current || User.default ]
38
41
  def members = users
39
42
  def memberships = NONE
40
43
  def integrations = NONE
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Railwatch
4
- VERSION = "0.2.0.pre1"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: railwatch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0.pre1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cole Robertson
@@ -204,6 +204,7 @@ files:
204
204
  - db/railwatch_migrate/20260916000000_create_railwatch_tables.rb
205
205
  - db/railwatch_migrate/20260917000000_create_railwatch_maintenance_tasks.rb
206
206
  - db/railwatch_migrate/20260917120000_create_railwatch_followup_receipts.rb
207
+ - db/railwatch_migrate/20260918120000_widen_host_user_ids.rb
207
208
  - db/railwatch_telemetry_migrate/20260903000001_create_telemetry.rb
208
209
  - db/railwatch_telemetry_migrate/20260903000002_rename_tenant_to_app_tenant.rb
209
210
  - db/railwatch_telemetry_migrate/20260903000003_add_statement_count_to_transactions.rb