Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 11 additions & 15 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ PATH
ruby-openai
stimulus-rails
turbo-rails
web-push (~> 3.0)

GEM
remote: https://rubygems.org/
Expand Down Expand Up @@ -109,8 +110,6 @@ GEM
ast (2.4.3)
base64 (0.3.0)
bcrypt_pbkdf (1.1.2)
bcrypt_pbkdf (1.1.2-arm64-darwin)
bcrypt_pbkdf (1.1.2-x86_64-darwin)
bigdecimal (4.0.1)
bindex (0.8.1)
bootsnap (1.23.0)
Expand All @@ -133,7 +132,6 @@ GEM
commonmarker (2.6.3-aarch64-linux)
commonmarker (2.6.3-arm-linux)
commonmarker (2.6.3-arm64-darwin)
commonmarker (2.6.3-x86_64-darwin)
commonmarker (2.6.3-x86_64-linux)
commonmarker (2.6.3-x86_64-linux-musl)
concurrent-ruby (1.3.6)
Expand Down Expand Up @@ -175,7 +173,6 @@ GEM
ffi (1.17.3-arm-linux-gnu)
ffi (1.17.3-arm-linux-musl)
ffi (1.17.3-arm64-darwin)
ffi (1.17.3-x86_64-darwin)
ffi (1.17.3-x86_64-linux-gnu)
ffi (1.17.3-x86_64-linux-musl)
formtastic (6.0.0)
Expand Down Expand Up @@ -217,6 +214,8 @@ GEM
actionview (>= 7.0.0)
activesupport (>= 7.0.0)
json (2.18.1)
jwt (3.1.2)
base64
kamal (2.10.1)
activesupport (>= 7.0)
base64 (~> 0.2)
Expand Down Expand Up @@ -289,12 +288,11 @@ GEM
racc (~> 1.4)
nokogiri (1.19.1-arm64-darwin)
racc (~> 1.4)
nokogiri (1.19.1-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-musl)
racc (~> 1.4)
openssl (4.0.1)
ostruct (0.6.3)
parallel (1.27.0)
parser (3.3.10.2)
Expand Down Expand Up @@ -471,7 +469,6 @@ GEM
thruster (0.1.18)
thruster (0.1.18-aarch64-linux)
thruster (0.1.18-arm64-darwin)
thruster (0.1.18-x86_64-darwin)
thruster (0.1.18-x86_64-linux)
timeout (0.6.0)
tsort (0.2.0)
Expand All @@ -490,6 +487,9 @@ GEM
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
web-push (3.1.0)
jwt (~> 3.0)
openssl (>= 3.0)
websocket (1.2.11)
websocket-driver (0.8.0)
base64
Expand All @@ -505,8 +505,7 @@ PLATFORMS
aarch64-linux-musl
arm-linux-gnu
arm-linux-musl
arm64-darwin
x86_64-darwin
arm64-darwin-25
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore Intel macOS platform support in lockfile

The lockfile now lists only arm64-darwin-25 for macOS and drops x86_64-darwin, which makes Bundler treat Intel Macs as unsupported when using a frozen/deployment lockfile. That blocks bundle install for contributors or CI runners on Intel macOS until the platform is re-added to Gemfile.lock.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3f2aa35 via bundle lock --add-platform x86_64-darwin arm64-darwin. Both are back in the lockfile so frozen bundle install works on Intel macOS again. (The original drop was an artifact of bundle install running on this arm64-darwin-25 machine.)

x86_64-linux
x86_64-linux-gnu
x86_64-linux-musl
Expand Down Expand Up @@ -566,8 +565,6 @@ CHECKSUMS
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6
bcrypt_pbkdf (1.1.2-arm64-darwin) sha256=afdd6feb6ed5a97b8e44caacb3f2d641b98af78e6a516d4a3520b69af5cf9fea
bcrypt_pbkdf (1.1.2-x86_64-darwin) sha256=35f5639d0058e6c2cc2f856f9c0b14080543268d3047abe6bc81c513093caa0e
bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e
bootsnap (1.23.0) sha256=c1254f458d58558b58be0f8eb8f6eec2821456785b7cdd1e16248e2020d3f214
Expand All @@ -578,7 +575,6 @@ CHECKSUMS
commonmarker (2.6.3-aarch64-linux) sha256=73795e80ab5ef1e4b5b83ada6f082bccb0ed7eae0b910232e13af1b2d71b14d6
commonmarker (2.6.3-arm-linux) sha256=62b9f32d7d3f85d47988a4a98a2e66e60ca42b894687047db8332f1e80caff7b
commonmarker (2.6.3-arm64-darwin) sha256=d6c1e4955619da3f68fed22de99dec49a24925611770c039bf870823846c8b21
commonmarker (2.6.3-x86_64-darwin) sha256=cd8ab974bb24f675a250ea91a811b3ff70405be1c219f0052446995db6ca90c6
commonmarker (2.6.3-x86_64-linux) sha256=e861ba1812721113725ebd8e46e4fee20dc732842f5555db2cfb8dcd74056583
commonmarker (2.6.3-x86_64-linux-musl) sha256=2c62d2dc0d5c4efc6dde39bc5c5fac292169206601a3daf75e562d70b795d49e
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
Expand Down Expand Up @@ -608,7 +604,6 @@ CHECKSUMS
ffi (1.17.3-arm-linux-gnu) sha256=5bd4cea83b68b5ec0037f99c57d5ce2dd5aa438f35decc5ef68a7d085c785668
ffi (1.17.3-arm-linux-musl) sha256=0d7626bb96265f9af78afa33e267d71cfef9d9a8eb8f5525344f8da6c7d76053
ffi (1.17.3-arm64-darwin) sha256=0c690555d4cee17a7f07c04d59df39b2fba74ec440b19da1f685c6579bb0717f
ffi (1.17.3-x86_64-darwin) sha256=1f211811eb5cfaa25998322cdd92ab104bfbd26d1c4c08471599c511f2c00bb5
ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
ffi (1.17.3-x86_64-linux-musl) sha256=086b221c3a68320b7564066f46fed23449a44f7a1935f1fe5a245bd89d9aea56
formtastic (6.0.0) sha256=c398906b65978fec3d045d6792f82cf9641f086ac9f17357b2b382f723126165
Expand All @@ -626,6 +621,7 @@ CHECKSUMS
irb (1.17.0) sha256=168c4ddb93d8a361a045c41d92b2952c7a118fa73f23fe14e55609eb7a863aae
jbuilder (2.14.1) sha256=4eb26376ff60ef100cb4fd6fd7533cd271f9998327e86adf20fd8c0e69fabb42
json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986
jwt (3.1.2) sha256=af6991f19a6bb4060d618d9add7a66f0eeb005ac0bc017cd01f63b42e122d535
kamal (2.10.1) sha256=53b7ecb4c33dd83b1aedfc7aacd1c059f835993258a552d70d584c6ce32b6340
kaminari (1.2.2) sha256=c4076ff9adccc6109408333f87b5c4abbda5e39dc464bd4c66d06d9f73442a3e
kaminari-actionview (1.2.2) sha256=1330f6fc8b59a4a4ef6a549ff8a224797289ebf7a3a503e8c1652535287cc909
Expand Down Expand Up @@ -658,9 +654,9 @@ CHECKSUMS
nokogiri (1.19.1-arm-linux-gnu) sha256=0a39ed59abe3bf279fab9dd4c6db6fe8af01af0608f6e1f08b8ffa4e5d407fa3
nokogiri (1.19.1-arm-linux-musl) sha256=3a18e559ee499b064aac6562d98daab3d39ba6cbb4074a1542781b2f556db47d
nokogiri (1.19.1-arm64-darwin) sha256=dfe2d337e6700eac47290407c289d56bcf85805d128c1b5a6434ddb79731cb9e
nokogiri (1.19.1-x86_64-darwin) sha256=7093896778cc03efb74b85f915a775862730e887f2e58d6921e3fa3d981e68bf
nokogiri (1.19.1-x86_64-linux-gnu) sha256=1a4902842a186b4f901078e692d12257678e6133858d0566152fe29cdb98456a
nokogiri (1.19.1-x86_64-linux-musl) sha256=4267f38ad4fc7e52a2e7ee28ed494e8f9d8eb4f4b3320901d55981c7b995fc23
openssl (4.0.1) sha256=e27974136b7b02894a1bce46c5397ee889afafe704a839446b54dc81cb9c5f7d
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357
Expand Down Expand Up @@ -717,7 +713,6 @@ CHECKSUMS
thruster (0.1.18) sha256=f025103bc7c8e6747436bb9de058c366840d2871560574ea7070a9bc8608a889
thruster (0.1.18-aarch64-linux) sha256=16f3d49468d76a9a5de86b7bdedf535b7b80da7c16495ca8ec96cfdc256870e2
thruster (0.1.18-arm64-darwin) sha256=8b297797a354ec6a81ea156b44279b66bff8da2404112f70f4ec515c2f276cc2
thruster (0.1.18-x86_64-darwin) sha256=355b6c0ee30ead7f7096448de4f0f9e8acc8454d2ef24b2d54965c5d813f1c67
thruster (0.1.18-x86_64-linux) sha256=0ec1ff5f12289c1ac10cf8e28ce6b5266f4e73416b34a664b79d037c7d955c40
timeout (0.6.0) sha256=6d722ad619f96ee383a0c557ec6eb8c4ecb08af3af62098a0be5057bf00de1af
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
Expand All @@ -728,6 +723,7 @@ CHECKSUMS
uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844
web-console (4.2.1) sha256=e7bcf37a10ea2b4ec4281649d1cee461b32232d0a447e82c786e6841fd22fe20
web-push (3.1.0) sha256=501ab00340c58fb70dabda59f28a86b3cccb0930c6f1f30721b2a5b24de7187d
websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737
websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962
websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
Expand Down
8 changes: 8 additions & 0 deletions config/initializers/coplan.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,12 @@
SlackNotificationJob.perform_later(comment_thread_id: payload[:comment_thread_id])
end
}

# Web Push (VAPID) keys for browser notifications. For local dev these are
# checked in; in production they should come from Rails encrypted credentials
# or your secrets manager. Generate fresh keys with:
# bundle exec rake coplan:web_push:generate_keys
config.vapid_public_key = ENV["COPLAN_VAPID_PUBLIC_KEY"] || "BPY5NsdGJ4vEmHHNz3SqK2XsmV93j-iR3-kqN-RMbl4JRd9jnKpzunwdXDwFwlzbRlPErn3x379e6Cz7DfdSS6o="
config.vapid_private_key = ENV["COPLAN_VAPID_PRIVATE_KEY"] || "1HoYR1d8QIlf8RYTfugJQFTyLlBat3zd-EFkj5dO9WQ="
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove checked-in fallback VAPID private key

This initializer hardcodes a usable VAPID private key as a default, so any environment that forgets to set COPLAN_VAPID_PRIVATE_KEY will silently run with a publicly known signing key. In that state, anyone who learns a subscription endpoint can forge push messages that appear to come from this app. This should fail closed in non-development environments instead of defaulting to a committed secret.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 3f2aa35. The fallback keypair now only applies in Rails.env.development?. In any other env, missing COPLAN_VAPID_* leaves the config nil, which makes web_push_configured? false and disables the meta tags / subscription endpoints / settings card entirely. Also rotated the dev keypair since the prior one is now in commit history.

config.vapid_subject = ENV["COPLAN_VAPID_SUBJECT"] || "mailto:dev@coplan.local"
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# This migration comes from co_plan (originally 20260508000000)
class CreateCoplanWebPushSubscriptions < ActiveRecord::Migration[8.0]
def change
create_table :coplan_web_push_subscriptions, id: :string, limit: 36 do |t|
t.string :user_id, null: false, limit: 36
t.text :endpoint, null: false
# SHA256 hex digest of endpoint, used for unique constraint since the
# full endpoint can exceed indexable string limits (FCM endpoints can
# be 200-400 chars).
t.string :endpoint_digest, null: false, limit: 64
t.string :p256dh_key, null: false, limit: 255
t.string :auth_key, null: false, limit: 100
t.string :user_agent
t.datetime :last_seen_at
t.datetime :last_delivered_at
t.integer :notifications_delivered_count, null: false, default: 0
t.timestamps
end

add_index :coplan_web_push_subscriptions, :user_id
add_index :coplan_web_push_subscriptions, :endpoint_digest, unique: true
add_foreign_key :coplan_web_push_subscriptions, :coplan_users, column: :user_id
end
end
19 changes: 18 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 81 additions & 0 deletions engine/app/assets/stylesheets/coplan/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -1428,18 +1428,99 @@ img.avatar {
color: var(--color-text-muted);
}

/* Honor [hidden] even on elements with explicit display rules (.btn etc.) */
[hidden] {
display: none !important;
}

/* Settings row */
.settings-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-md);
}

.settings-row__label {
font-weight: 600;
color: var(--color-text);
}

.settings-row__main {
flex: 1 1 auto;
min-width: 0;
}

.settings-row__hint {
font-size: var(--text-sm);
color: var(--color-text-muted);
margin-top: var(--space-xs);
line-height: 1.5;
}

.settings-row__action {
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: var(--space-xs);
}

.settings-row__status {
font-size: var(--text-sm);
color: var(--color-text-muted);
}

.settings-subsection {
margin-top: var(--space-md);
padding-top: var(--space-md);
border-top: 1px solid var(--color-border);
}

.settings-subsection__heading {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: var(--space-sm);
}

.settings-subsection__hint {
font-size: var(--text-sm);
color: var(--color-text-muted);
margin-top: var(--space-sm);
margin-bottom: 0;
}

.device-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-sm);
}

.device-list__item {
padding: var(--space-sm) var(--space-md);
background: var(--color-surface-alt, var(--color-surface));
border: 1px solid var(--color-border);
border-radius: var(--radius);
}

.device-list__name {
font-weight: 500;
color: var(--color-text);
font-size: var(--text-sm);
}

.device-list__meta {
font-size: var(--text-xs);
color: var(--color-text-muted);
margin-top: var(--space-xs);
}

/* Theme switcher (segmented control) */
.theme-switcher {
display: inline-flex;
Expand Down
23 changes: 23 additions & 0 deletions engine/app/controllers/coplan/service_workers_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module CoPlan
class ServiceWorkersController < ApplicationController
# Service worker registration runs from the browser without our session
# cookie, and the SW URL needs to be public anyway.
skip_before_action :authenticate_coplan_user!
skip_before_action :verify_authenticity_token, raise: false

SW_PATH = CoPlan::Engine.root.join("app/javascript/coplan_service_worker.js")
SW_BODY = SW_PATH.read.freeze

def show
# No Service-Worker-Allowed header on purpose: default scope is the
# SW's own directory, which is the engine mount point. Push events
# fire regardless of scope, so this doesn't limit notification reach.
#
# Render inline rather than send_file: the JS lives inside the gem,
# so any reverse proxy that intercepts X-Sendfile (NGINX et al) won't
# reach it. The file is small and cached in memory at boot.
response.headers["Cache-Control"] = "no-cache"
render plain: SW_BODY, content_type: "application/javascript"
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module Settings
class SettingsController < ApplicationController
def index
@api_tokens = current_user.api_tokens.order(created_at: :desc)
@web_push_subscriptions = current_user.web_push_subscriptions.order(created_at: :desc)
end

def update_theme
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module CoPlan
module WebPush
class SubscriptionsController < ApplicationController
before_action :require_web_push_configured

# POST /web_push/subscription
# Body: { subscription: { endpoint, keys: { p256dh, auth } } }
def create
sub_params = subscription_params
record = WebPushSubscription.upsert_for(
user: current_user,
endpoint: sub_params[:endpoint],
p256dh_key: sub_params.dig(:keys, :p256dh),
auth_key: sub_params.dig(:keys, :auth),
user_agent: request.user_agent&.truncate(255)
)
render json: { id: record.id, created_at: record.created_at }, status: :created
end

# DELETE /web_push/subscription
# Body: { subscription: { endpoint } }
def destroy
endpoint = subscription_params[:endpoint]
digest = WebPushSubscription.digest_for(endpoint)
# Scope to current_user so a leaked endpoint can't unsubscribe
# someone else.
deleted = current_user.web_push_subscriptions.where(endpoint_digest: digest).delete_all
head deleted.positive? ? :no_content : :not_found
end

private

def subscription_params
params.require(:subscription).permit(:endpoint, keys: [:p256dh, :auth])
end

def require_web_push_configured
head :service_unavailable unless CoPlan.configuration.web_push_configured?
end
end
end
end
Loading
Loading