Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
035abfa
wip
seanmarcia Feb 3, 2026
27ab546
Merge feature/device-management-api-keys into beacons branch
seanmarcia Feb 3, 2026
1b77f29
Update beacon UI to use API key system from feature/device-management…
seanmarcia Feb 3, 2026
b4cfadd
Fix topic field reference - use 'title' instead of 'name'
seanmarcia Feb 3, 2026
b4540fa
Improve API key display and add regeneration feature
seanmarcia Feb 3, 2026
fb7b72c
Add document and file count to beacon show page
seanmarcia Feb 3, 2026
16a8485
Wip
seanmarcia Feb 3, 2026
ee1549b
Regenerate and Revoke Tokens on Beacon Configuration Admin View
FionaLMcLaren Feb 3, 2026
ca83796
Adds Request Spec for Beacons, and route for revoking a Beacon's API
FionaLMcLaren Feb 4, 2026
0a53932
chore(deps): bump bootsnap from 1.20.1 to 1.22.0 (#576)
dependabot[bot] Feb 3, 2026
9056305
chore(deps): bump tailwindcss-rails from 4.3.0 to 4.4.0 (#577)
dependabot[bot] Feb 3, 2026
dd6b8e9
chore(deps): bump thruster from 0.1.17 to 0.1.18 (#578)
dependabot[bot] Feb 3, 2026
b459b7e
Update Favicon (#563)
seanmarcia Feb 3, 2026
b37d46e
Upgrade to Ruby 4.0.1 (#570)
devjona Feb 3, 2026
4727750
Add macOS 15 platform to Gemfile.lock
oatkins8 Feb 3, 2026
75f1500
chore(deps-dev): bump brakeman from 8.0.1 to 8.0.2 (#583)
dependabot[bot] Feb 4, 2026
07a9fdf
Tailwind migration cleanup – sidebar (#584)
hydrognomik Feb 4, 2026
10a48ea
Fixing error - turns out did not call `set_beacon` for `revoke_key`
FionaLMcLaren Feb 4, 2026
3954d6e
Gets rid of the redundant specs in the `Beacon` request spec
FionaLMcLaren Feb 4, 2026
c125a91
Merge branch 'main' into beacons-regenerate
FionaLMcLaren Feb 4, 2026
a24368c
Review changes
FionaLMcLaren Feb 4, 2026
8dd9f8e
Review changes
FionaLMcLaren Feb 4, 2026
c595113
More review changes
FionaLMcLaren Feb 13, 2026
99069e6
Review changes:
FionaLMcLaren Mar 2, 2026
a1d7031
Merge branch 'main' into beacons-regenerate
dmitrytrager Mar 23, 2026
2c5ffc1
rescue from StandardError in beacons controller
dmitrytrager Mar 23, 2026
107c2de
eliminate beacons helper, use method from model
dmitrytrager Mar 23, 2026
d6a6a4e
move beacons out of redundant beacons module
dmitrytrager Mar 23, 2026
d2e4b6f
fix(beacons): use explicit click event in Stimulus action descriptor
dmitrytrager Mar 23, 2026
04aa30e
feat(beacons): filter topics and providers based on selected language…
dmitrytrager Mar 23, 2026
b07c42f
fix: resolve code style issues
dmitrytrager Mar 23, 2026
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
46 changes: 46 additions & 0 deletions app/assets/tailwind/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -387,3 +387,49 @@ nav.pagy a.current {
.help-text {
@apply mt-2 text-xs text-gray-500 m-0;
}

/* Beacon Configuration Admin View styles */
.beacon-online {
display: inline-block;
width: 10px;
height: 10px;
background-color: #10b981;
border-radius: 50%;
animation: pulse-green 2s infinite;
}

.beacon-online-large {
display: inline-block;
width: 16px;
height: 16px;
background-color: #10b981;
border-radius: 50%;
animation: pulse-green 2s infinite;
}

.beacon-offline {
display: inline-block;
width: 10px;
height: 10px;
background-color: #ef4444;
border-radius: 50%;
}

.beacon-offline-large {
display: inline-block;
width: 16px;
height: 16px;
background-color: #ef4444;
border-radius: 50%;
}

@keyframes pulse-green {
0%, 100% {
opacity: 1;
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
}
50% {
opacity: 0.8;
box-shadow: 0 0 0 6px rgba(16, 185, 129, 0);
}
}
4 changes: 4 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ def current_provider
end
end
helper_method :current_provider

def non_contributor_redirect_path
topics_path
end
end
99 changes: 99 additions & 0 deletions app/controllers/beacons_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
class BeaconsController < ApplicationController
include Authentication

before_action :set_beacon, only: %i[show edit update regenerate_key revoke_key]
before_action :prepare_associations, only: %i[new edit]

def index
@beacons = Beacon.includes(:language, :region, :providers, :topics).order(created_at: :desc)
end

def new
@beacon = Beacon.new
end

def create
success, @beacon, api_key = Beacons::Creator.new.call(beacon_params)

if success
flash[:notice] = "Beacon was successfully provisioned. API Key: #{api_key}"
redirect_to beacon_path(@beacon, api_key: api_key)
else
prepare_associations
render :new, status: :unprocessable_entity
end
end

def show; end

def edit; end

def update
if @beacon.update(beacon_params)
redirect_to @beacon, notice: "Beacon was successfully updated."
else
prepare_associations
render :edit, status: :unprocessable_entity
end
end

def regenerate_key
_, api_key = Beacons::KeyRegenerator.new.call(@beacon)
flash[:notice] = "API key has been successfully regenerated. API Key: #{api_key}"
redirect_to beacon_path(@beacon, api_key: api_key)

rescue => StandardError
flash[:alert] = "API key could not be regenerated."
redirect_to @beacon
end

def filter_options
topics = if params[:language_id].present?
Topic.active.where(language_id: params[:language_id]).order(:title)
else
Topic.active.order(:title)
end

providers = if params[:region_id].present?
Provider.joins(:branches).where(branches: { region_id: params[:region_id] }).distinct.order(:name)
else
Provider.order(:name)
end

render json: {
topics: topics.select(:id, :title),
providers: providers.select(:id, :name),
}
end

def revoke_key
api_key = @beacon.revoke!
flash[:notice] = "API key has been successfully revoked."
redirect_to @beacon

rescue => StandardError
flash[:alert] = "API key could not be revoked."
redirect_to @beacon
end

def non_contributor_redirect_path
root_path
end

private

def set_beacon
@beacon = Beacon.find(params[:id])
end

def prepare_associations
@languages = Language.order(:name)
@providers = Provider.order(:name)
@regions = Region.order(:name)
@topics = Topic.active.order(:title)
end

def beacon_params
params.require(:beacon).permit(:name, :language_id, :region_id, provider_ids: [], topic_ids: [])
end
end
2 changes: 1 addition & 1 deletion app/controllers/concerns/authentication.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def allow_unauthenticated_access(**options)
end

def redirect_contributors
redirect_to topics_path unless Current.user.is_admin?
redirect_to non_contributor_redirect_path unless Current.user.is_admin?
end

private
Expand Down
51 changes: 51 additions & 0 deletions app/javascript/controllers/beacon_form_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Controller } from "@hotwired/stimulus"
import { get } from "@rails/request.js"

export default class extends Controller {
static targets = ["languageSelect", "regionSelect", "topicsSelect", "providersSelect"]
static values = { filterUrl: String }

connect() {
// Defer until select-tags children have initialized their TomSelect instances
requestAnimationFrame(() => this.fetchOptions())
}

fetchOptions() {
const params = new URLSearchParams()
const languageId = this.languageSelectTarget.value
const regionId = this.regionSelectTarget.value

if (languageId) params.set("language_id", languageId)
if (regionId) params.set("region_id", regionId)

get(`${this.filterUrlValue}?${params}`, { responseKind: "json" })
.then(response => response.json)
.then(data => {
this.#updateSelect(this.topicsSelectTarget, data.topics, "title")
this.#updateSelect(this.providersSelectTarget, data.providers, "name")
})
}

#updateSelect(selectElement, items, textKey) {
const tomSelect = selectElement.tomselect
if (!tomSelect) return

const previousValues = [].concat(tomSelect.getValue())

tomSelect.clear(true)
tomSelect.clearOptions()

items.forEach(item => {
tomSelect.addOption({ value: String(item.id), text: item[textKey] })
})

const stillValidValues = previousValues.filter(v =>
items.some(item => String(item.id) === v)
)
if (stillValidValues.length > 0) {
tomSelect.setValue(stillValidValues, true)
}

tomSelect.refreshOptions(false)
}
}
24 changes: 24 additions & 0 deletions app/javascript/controllers/beacons_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["button", "beaconApiKey"]

copyApiKey() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure this copy functionality is working (tried locally).

Copy link
Collaborator

Choose a reason for hiding this comment

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

We could add to a later ticket, but the user can still see the API Key and manually select, copy.

Copy link
Collaborator

Choose a reason for hiding this comment

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

What was the outcome in your case?

const apiKey = this.beaconApiKeyTarget.textContent;

navigator.clipboard.writeText(apiKey).then(() => {
const button = this.buttonTarget;
const originalText = button.textContent;
button.textContent = "Copied!";
button.classList.add("text-green-600");

setTimeout(() => {
button.textContent = originalText;
button.classList.remove("text-green-600");
}, 2000);
}).catch(err => {
console.error("Failed to copy:", err);
alert("Failed to copy API key to clipboard");
});
}
}
17 changes: 17 additions & 0 deletions app/models/beacon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ class Beacon < ApplicationRecord
has_many :beacon_topics, dependent: :destroy
has_many :topics, through: :beacon_topics

delegate :name, to: :region, prefix: true
delegate :name, to: :language, prefix: true

validates :name, presence: true
validates :api_key_digest, presence: true, uniqueness: true
validates :api_key_prefix, presence: true
Expand All @@ -53,6 +56,20 @@ def revoked?
revoked_at.present?
end

def status_str
revoked? ? "Revoked" : "Active"
end

# Get count of topics that match this beacon's configuration
def document_count
topics.count
end

# Get count of actual document files attached to matching topics
def file_count
topics.joins(:documents_attachments).count
end

def accessible_blobs
ActiveStorage::Blob
.joins(:attachments)
Expand Down
Loading