-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "hanami/action"
-
1
require "initable"
-
-
1
module Terminus
-
# The application base action.
-
1
class Action < Hanami::Action
-
1
include Dry::Monads[:result]
-
-
1
before :authorize
-
-
1
protected
-
-
1
def authorize request, response
-
465
rodauth = request.env["rodauth"]
-
-
465
else: 279
then: 186
return unless rodauth
-
-
558
handle_rodauth_redirect(rodauth, response) { rodauth.require_account }
-
-
274
response[:current_user_id] = rodauth.account_id
-
end
-
-
1
private
-
-
1
def handle_rodauth_redirect rodauth, response
-
558
halted = catch(:halt) { yield }
-
-
skipped
# :nocov:
-
skipped
return unless halted
-
skipped
-
skipped
code, headers, body = *halted
-
skipped
-
skipped
rodauth.flash.next.each { |key, value| response.flash[key] = value }
-
skipped
response.redirect headers["Location"], code
-
skipped
-
skipped
throw :halt, [code, body]
-
skipped
# :nocov:
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "petail"
-
-
1
require_relative "../../aspects/problem_detail"
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
# The base action.
-
1
class Base < Action
-
1
config.formats.accept :json
-
1
handle_exception Dry::Types::SchemaError => :detail_enum,
-
ROM::SQL::UniqueConstraintError => :detail_duplicate,
-
ROM::SQL::ForeignKeyConstraintError => :detail_foreign_key
-
-
1
using Refines::Actions::Response
-
-
1
def initialize(problem: Petail, problem_detail: Aspects::ProblemDetail, **)
-
92
@problem = problem
-
92
@problem_detail = problem_detail
-
92
super(**)
-
end
-
-
1
protected
-
-
1
attr_reader :problem
-
-
1
def verify_csrf_token?(*) = false
-
-
1
private
-
-
1
attr_reader :problem_detail
-
-
1
def detail_duplicate request, response, error
-
1
payload = problem_detail.duplicate error.message, request.path
-
1
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
-
1
def detail_enum request, response, error
-
1
payload = problem_detail.enum error.message, request.path
-
1
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
-
1
def detail_foreign_key request, response, error
-
1
payload = problem_detail.foreign_key error.message, request.path
-
1
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Devices
-
# The create action.
-
1
class Create < Base
-
1
include Deps["aspects.devices.provisioner"]
-
1
include Initable[serializer: Serializers::Device]
-
-
1
using Refines::Actions::Response
-
-
1
contract Contracts::Devices::Create
-
-
1
def handle request, response
-
4
parameters = request.params
-
-
4
then: 3
if parameters.valid?
-
3
process parameters, response
-
else: 1
else
-
1
unprocessable_content parameters, response
-
end
-
end
-
-
1
private
-
-
1
def process parameters, response
-
3
in: 2
case provisioner.call(**parameters[:device])
-
2
in: 1
in Success(device) then response.body = {data: serializer.new(device).to_h}.to_json
-
1
in Failure(String => error) then not_found error, response
-
skipped
# :nocov:
-
skipped
# :nocov:
-
end
-
end
-
-
1
def not_found error, response
-
1
payload = problem[
-
type: "/problem_details#device_payload",
-
status: __method__,
-
detail: error,
-
instance: "/api/devices"
-
]
-
-
1
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
-
1
def unprocessable_content parameters, response
-
1
payload = problem[
-
type: "/problem_details#device_payload",
-
status: :unprocessable_content,
-
detail: "Validation failed.",
-
instance: "/api/devices",
-
extensions: {errors: parameters.errors.to_h}
-
]
-
-
1
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Devices
-
# The delete action.
-
1
class Delete < Base
-
1
include Deps[repository: "repositories.device"]
-
1
include Initable[serializer: Serializers::Device]
-
-
1
def handle request, response
-
2
device = repository.delete request.params[:id]
-
2
response.body = {data: serializer.new(device).to_h}.to_json
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Devices
-
# The index action.
-
1
class Index < Base
-
1
include Deps[repository: "repositories.device"]
-
1
include Initable[serializer: Serializers::Device]
-
-
1
def handle *, response
-
3
data = repository.all.map { serializer.new(it).to_h }
-
2
response.body = {data:}.to_json
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Devices
-
# The patch action.
-
1
class Patch < Base
-
1
include Deps[repository: "repositories.device"]
-
1
include Initable[serializer: Serializers::Device]
-
-
1
using Refines::Actions::Response
-
-
1
contract Contracts::Devices::Patch
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
then: 1
if parameters.valid?
-
1
device = repository.update(*parameters.to_h.values_at(:id, :device))
-
1
response.body = {data: serializer.new(device).to_h}.to_json
-
else: 2
else
-
2
unprocessable_content parameters, response
-
end
-
end
-
-
1
private
-
-
1
def unprocessable_content parameters, response
-
2
payload = problem[
-
type: "/problem_details#device_payload",
-
status: :unprocessable_content,
-
detail: "Validation failed.",
-
instance: "/api/devices",
-
extensions: {errors: parameters.errors.to_h}
-
]
-
-
2
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Devices
-
# The show action.
-
1
class Show < Base
-
1
include Deps[repository: "repositories.device"]
-
1
include Initable[serializer: Serializers::Device]
-
-
1
def handle request, response
-
2
device = repository.find request.params[:id]
-
-
2
then: 1
response.body = if device
-
1
{data: serializer.new(device).to_h}.to_json
-
else: 1
else
-
1
problem[status: :not_found].to_json
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "trmnl/api"
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Display
-
# The show action.
-
# :reek:DataClump
-
1
class Show < Base
-
1
include Deps[
-
:settings,
-
"aspects.devices.synchronizer",
-
"aspects.screens.rotator",
-
"aspects.screens.gaffer",
-
firmware_repository: "repositories.firmware"
-
]
-
-
1
include Initable[model: TRMNL::API::Models::Display]
-
-
1
using Refines::Actions::Response
-
-
1
def handle request, response
-
9
in: 6
case synchronizer.call request.env
-
6
else: 3
in Success(device) then rotate device, response
-
3
else not_found response
-
end
-
end
-
-
1
protected
-
-
1
def authorize(*) = nil
-
-
1
private
-
-
1
def rotate device, response
-
6
rotator.call(device)
-
4
.either -> screen { success device, screen, response },
-
2
-> message { error_for device, message, response }
-
end
-
-
1
def success device, screen, response
-
attributes = {
-
4
filename: screen.image_name_with_checksum,
-
image_url: screen.image_uri(host: settings.api_uri)
-
}
-
-
4
response.body = build_payload(device, attributes).to_json
-
end
-
-
1
def error_for device, message, response
-
4
gaffer.call(device, message).bind { |screen| any_error device, screen, response }
-
end
-
-
1
def build_payload device, attributes
-
4
model[**fetch_firmware(device), **attributes, **device.as_api_display]
-
end
-
-
1
def fetch_firmware device
-
6
firmware_repository.latest.then do |firmware|
-
6
else: 3
then: 3
break unless firmware
-
-
3
version = firmware.version
-
-
3
then: 1
else: 2
break if device.firmware_version == version
-
-
{
-
2
firmware_url: firmware.attachment_uri(host: settings.api_uri),
-
firmware_version: version
-
}
-
end
-
end
-
-
1
def any_error device, screen, response
-
2
payload = model[
-
filename: screen.image_name,
-
image_url: screen.image_uri(host: settings.api_uri),
-
**fetch_firmware(device),
-
**device.as_api_display
-
]
-
-
2
response.body = payload.to_json
-
end
-
-
1
def not_found response
-
3
payload = problem[
-
type: "/problem_details#device_id",
-
status: __method__,
-
detail: "Invalid device ID.",
-
instance: "/api/display"
-
]
-
-
3
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/pathname"
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Firmware
-
# The create action.
-
1
class Create < Base
-
1
include Deps["aspects.downloader", repository: "repositories.firmware"]
-
1
include Initable[serializer: Serializers::Firmware]
-
-
1
using Refines::Actions::Response
-
1
using Refinements::Pathname
-
-
1
params do
-
1
required(:firmware).filled :hash do
-
1
required(:version).filled Types::Version
-
1
required(:kind).filled :string
-
1
required(:uri).filled :string
-
end
-
end
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
then: 2
if parameters.valid?
-
2
process parameters[:firmware], response
-
else: 1
else
-
1
unprocessable_content parameters, response
-
end
-
end
-
-
1
private
-
-
1
def process parameters, response
-
2
uri = parameters.delete :uri
-
2
record = repository.create parameters
-
-
2
downloader.call(uri)
-
1
.either -> http_response { upload record, http_response.body.to_s, response },
-
1
proc { unprocessable_download uri, response }
-
end
-
-
# :reek:FeatureEnvy
-
# :reek:TooManyStatements
-
1
def upload record, content, response
-
1
Pathname.mktmpdir do |root|
-
2
root.join("#{record.version}.bin").write(content).open { record.upload it }
-
end
-
-
1
update = repository.update record.id, attachment_data: record.attachment_attributes
-
-
1
response.body = {data: serializer.new(update).to_h}.to_json
-
end
-
-
1
def unprocessable_download uri, response
-
1
payload = problem[
-
type: "/problem_details#firmware_payload",
-
status: :unprocessable_content,
-
detail: "Invalid URI: #{uri}.",
-
instance: "/api/firmware"
-
]
-
-
1
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
-
1
def unprocessable_content parameters, response
-
1
payload = problem[
-
type: "/problem_details#firmware_payload",
-
status: :unprocessable_content,
-
detail: "Validation failed.",
-
instance: "/api/firmware",
-
extensions: {errors: parameters.errors.to_h}
-
]
-
-
1
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Firmware
-
# The delete action.
-
1
class Delete < Base
-
1
include Deps[repository: "repositories.firmware"]
-
1
include Initable[serializer: Serializers::Firmware]
-
-
1
using Refines::Actions::Response
-
-
1
def handle request, response
-
2
repository.find(request.params[:id]).then do |firmware|
-
2
then: 1
else: 1
firmware ? success(firmware, response) : failure(response)
-
end
-
end
-
-
1
private
-
-
1
def success firmware, response
-
1
repository.delete firmware.id
-
1
response.body = {data: serializer.new(firmware).to_h}.to_json
-
end
-
-
1
def failure response
-
1
payload = problem[status: :not_found]
-
1
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Firmware
-
# The index action.
-
1
class Index < Base
-
1
include Deps[repository: "repositories.firmware"]
-
1
include Initable[serializer: Serializers::Firmware]
-
-
1
def handle *, response
-
3
data = repository.all.map { serializer.new(it).to_h }
-
2
response.body = {data:}.to_json
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/pathname"
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Firmware
-
# The patch action.
-
1
class Patch < Base
-
1
include Deps["aspects.downloader", repository: "repositories.firmware"]
-
1
include Initable[serializer: Serializers::Firmware]
-
-
1
using Refines::Actions::Response
-
1
using Refinements::Pathname
-
-
1
params do
-
1
required(:id).filled :integer
-
-
1
required(:firmware).filled :hash do
-
1
optional(:version).filled Types::Version
-
1
optional(:kind).filled :string
-
1
optional(:uri).filled :string
-
end
-
end
-
-
1
def handle request, response
-
4
parameters = request.params
-
-
4
then: 3
if parameters.valid?
-
3
process(*parameters.to_h.values_at(:id, :firmware), response)
-
else: 1
else
-
1
unprocessable_content parameters, response
-
end
-
end
-
-
1
private
-
-
1
def process id, parameters, response
-
3
uri = parameters.delete :uri
-
3
record = repository.update id, parameters
-
-
3
else: 2
then: 1
return response.with body: {data: serializer.new(record).to_h}.to_json unless uri
-
-
2
download uri, record, response
-
end
-
-
1
def download uri, record, response
-
2
downloader.call(uri)
-
1
.either -> payload { replace record, payload.body.to_s, response },
-
1
proc { unprocessable_download uri, response }
-
end
-
-
# :reek:FeatureEnvy
-
1
def replace record, content, response
-
1
Pathname.mktmpdir do |root|
-
2
root.join("#{record.version}.bin").write(content).open { record.replace it }
-
end
-
-
1
update = repository.update record.id, attachment_data: record.attachment_attributes
-
-
1
response.with body: {data: serializer.new(update).to_h}.to_json
-
end
-
-
1
def unprocessable_download uri, response
-
1
payload = problem[
-
type: "/problem_details#firmware_payload",
-
status: :unprocessable_content,
-
detail: "Invalid URI: #{uri}.",
-
instance: "/api/firmware"
-
]
-
-
1
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
-
1
def unprocessable_content parameters, response
-
1
payload = problem[
-
type: "/problem_details#firmware_payload",
-
status: :unprocessable_content,
-
detail: "Validation failed.",
-
instance: "/api/firmware",
-
extensions: {errors: parameters.errors.to_h}
-
]
-
-
1
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Firmware
-
# The show action.
-
1
class Show < Base
-
1
include Deps[repository: "repositories.firmware"]
-
1
include Initable[serializer: Serializers::Firmware]
-
-
1
def handle request, response
-
2
firmware = repository.find request.params[:id]
-
-
2
then: 1
response.body = if firmware
-
1
{data: serializer.new(firmware).to_h}.to_json
-
else: 1
else
-
1
problem[status: :not_found].to_json
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Log
-
# The create action.
-
1
class Create < Base
-
1
include Deps[
-
:logger,
-
transformer: "aspects.firmware.log_transformer",
-
device_repository: "repositories.device",
-
log_repository: "repositories.device_log"
-
]
-
-
1
using Refines::Actions::Response
-
-
1
params do
-
1
required(:logs).filled(:array).each(:hash) do
-
1
required(:battery_voltage).filled :float
-
1
required(:created_at).filled :integer
-
1
required(:firmware_version).filled :string
-
1
required(:free_heap_size).filled :integer
-
1
required(:max_alloc_size).filled :integer
-
1
required(:id).filled :integer
-
1
required(:message).filled :string
-
1
required(:refresh_rate).filled :integer
-
1
optional(:retry).filled :integer
-
1
required(:sleep_duration).filled :integer
-
1
required(:source_line).filled :integer
-
1
required(:source_path).filled :string
-
1
required(:special_function).filled :string
-
1
required(:wake_reason).filled :string
-
1
required(:wifi_signal).filled :integer
-
1
required(:wifi_status).filled :string
-
end
-
end
-
-
1
def handle request, response
-
8
parameters = request.params
-
8
device = device_repository.find_by mac_address: request.get_header("HTTP_ID")
-
-
8
else: 6
then: 2
return not_found response unless device
-
6
else: 2
then: 4
return unprocessable_content parameters, response unless parameters.valid?
-
-
2
save device, parameters, response
-
end
-
-
1
protected
-
-
1
def authorize(*) = nil
-
-
1
private
-
-
1
def save device, parameters, response
-
2
transformer.call(parameters.to_h).each do |attributes|
-
2
log_repository.create attributes.merge!(device_id: device.id)
-
end
-
-
2
response.status = 204
-
end
-
-
1
def not_found response
-
2
payload = problem[
-
type: "/problem_details#device_id",
-
status: __method__,
-
detail: "Invalid device ID.",
-
instance: "/api/log"
-
]
-
-
2
logger.error "Unable to find device."
-
2
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
-
1
def unprocessable_content parameters, response
-
4
errors = parameters.errors.to_h
-
-
4
payload = problem[
-
type: "/problem_details#log_payload",
-
status: __method__,
-
detail: "Validation failed due to incorrect or invalid payload.",
-
instance: "/api/log",
-
extensions: {errors:}
-
]
-
-
4
logger.error errors
-
4
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Models
-
# The create action.
-
1
class Create < Base
-
1
include Deps[repository: "repositories.model"]
-
1
include Initable[serializer: Serializers::Model]
-
-
1
using Refines::Actions::Response
-
-
1
params do
-
1
required(:model).filled(:hash) do
-
1
optional(:default_palette_id).maybe :integer
-
1
required(:name).filled :string
-
1
required(:label).filled :string
-
1
optional(:description).maybe :string
-
1
optional(:mime_type).filled :string
-
1
optional(:bit_depth).filled :integer
-
1
optional(:colors).filled :integer
-
1
optional(:scale_factor).filled :float
-
1
optional(:rotation).filled :integer
-
1
optional(:offset_x).filled :integer
-
1
optional(:offset_y).filled :integer
-
1
optional(:css).maybe :hash
-
1
optional(:width).filled :integer
-
1
optional(:height).filled :integer
-
end
-
end
-
-
1
def handle request, response
-
2
parameters = request.params
-
-
2
then: 1
if parameters.valid?
-
1
model = repository.create parameters[:model]
-
1
response.body = {data: serializer.new(model).to_h}.to_json
-
else: 1
else
-
1
unprocessable_content parameters, response
-
end
-
end
-
-
1
private
-
-
1
def unprocessable_content parameters, response
-
1
payload = problem[
-
type: "/problem_details#model_payload",
-
status: :unprocessable_content,
-
detail: "Validation failed.",
-
instance: "/api/models",
-
extensions: {errors: parameters.errors.to_h}
-
]
-
-
1
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Models
-
# The delete action.
-
1
class Delete < Base
-
1
include Deps[repository: "repositories.model"]
-
1
include Initable[serializer: Serializers::Model]
-
-
1
def handle request, response
-
2
model = repository.delete request.params[:id]
-
2
response.body = {data: serializer.new(model).to_h}.to_json
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Models
-
# The index action.
-
1
class Index < Base
-
1
include Deps[repository: "repositories.model"]
-
1
include Initable[serializer: Serializers::Model]
-
-
1
def handle *, response
-
3
data = repository.all.map { serializer.new(it).to_h }
-
2
response.body = {data:}.to_json
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Models
-
# The patch action.
-
1
class Patch < Base
-
1
include Deps[repository: "repositories.model"]
-
1
include Initable[serializer: Serializers::Model]
-
-
1
using Refines::Actions::Response
-
-
1
params do
-
1
required(:id).filled :integer
-
-
1
required(:model).filled(:hash) do
-
1
optional(:default_palette_id).maybe :integer
-
1
optional(:name).filled :string
-
1
optional(:label).filled :string
-
1
optional(:description).maybe :string
-
1
optional(:mime_type).filled :string
-
1
optional(:bit_depth).filled :integer
-
1
optional(:colors).filled :integer
-
1
optional(:scale_factor).filled :float
-
1
optional(:rotation).filled :integer
-
1
optional(:offset_x).filled :integer
-
1
optional(:offset_y).filled :integer
-
1
optional(:css).maybe :hash
-
1
optional(:width).filled :integer
-
1
optional(:height).filled :integer
-
end
-
end
-
-
1
def handle request, response
-
2
parameters = request.params
-
-
2
then: 1
if parameters.valid?
-
1
model = repository.update(*parameters.to_h.values_at(:id, :model))
-
1
response.body = {data: serializer.new(model).to_h}.to_json
-
else: 1
else
-
1
unprocessable_content parameters, response
-
end
-
end
-
-
1
private
-
-
1
def unprocessable_content parameters, response
-
1
payload = problem[
-
type: "/problem_details#model_payload",
-
status: :unprocessable_content,
-
detail: "Validation failed.",
-
instance: "/api/models",
-
extensions: {errors: parameters.errors.to_h}
-
]
-
-
1
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Models
-
# The show action.
-
1
class Show < Base
-
1
include Deps[repository: "repositories.model"]
-
1
include Initable[serializer: Serializers::Model]
-
-
1
def handle request, response
-
2
model = repository.find request.params[:id]
-
-
2
then: 1
response.body = if model
-
1
{data: serializer.new(model).to_h}.to_json
-
else: 1
else
-
1
problem[status: :not_found].to_json
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Playlists
-
# The create action.
-
1
class Create < Base
-
1
include Deps[
-
repository: "repositories.playlist",
-
item_repository: "repositories.playlist_item"
-
]
-
-
1
include Initable[serializer: Serializers::Playlist]
-
-
1
using Refines::Actions::Response
-
-
1
params do
-
1
required(:playlist).filled(:hash) do
-
1
required(:name).filled :string
-
1
required(:label).filled :string
-
1
optional(:mode).filled :string
-
2
optional(:items).maybe(:array).each(:hash) { required(:screen_id).filled :integer }
-
end
-
end
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
then: 2
if parameters.valid?
-
2
playlist = save parameters[:playlist]
-
2
response.body = {data: serializer.new(playlist).to_h}.to_json
-
else: 1
else
-
1
unprocessable_content parameters, response
-
end
-
end
-
-
1
private
-
-
1
def save attributes
-
2
items = attributes.fetch :items, Core::EMPTY_ARRAY
-
2
playlist = repository.create_with_items attributes, items
-
2
repository.with_items.by_pk(playlist.id).one
-
end
-
-
1
def unprocessable_content parameters, response
-
1
payload = problem[
-
type: "/problem_details#playlist_payload",
-
status: :unprocessable_content,
-
detail: "Validation failed.",
-
instance: "/api/playlists",
-
extensions: {errors: parameters.errors.to_h}
-
]
-
-
1
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Playlists
-
# The delete action.
-
1
class Delete < Base
-
1
include Deps[repository: "repositories.playlist"]
-
1
include Initable[serializer: Serializers::Playlist]
-
-
1
def handle request, response
-
2
playlist = repository.with_items.by_pk(request.params[:id]).one
-
-
2
then: 1
response.body = if playlist
-
1
repository.delete playlist.id
-
1
{data: serializer.new(playlist).to_h}.to_json
-
else: 1
else
-
1
{data: {}}.to_json
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Playlists
-
# The index action.
-
1
class Index < Base
-
1
include Deps[repository: "repositories.playlist"]
-
1
include Initable[serializer: Serializers::Playlist]
-
-
1
def handle *, response
-
3
data = repository.with_items.to_a.map { serializer.new(it).to_h }
-
2
response.body = {data:}.to_json
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Playlists
-
# The patch action.
-
1
class Patch < Base
-
1
include Deps[repository: "repositories.playlist"]
-
1
include Initable[serializer: Serializers::Playlist]
-
-
1
using Refines::Actions::Response
-
-
1
params do
-
1
required(:id).filled :integer
-
-
1
required(:playlist).filled(:hash) do
-
1
optional(:current_item_id).filled :integer
-
1
required(:name).filled :string
-
1
required(:label).filled :string
-
1
optional(:mode).filled :string
-
2
optional(:items).maybe(:array).each(:hash) { required(:screen_id).filled :integer }
-
end
-
end
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
then: 2
if parameters.valid?
-
2
playlist = update parameters
-
2
response.body = {data: serializer.new(playlist).to_h}.to_json
-
else: 1
else
-
1
unprocessable_content parameters, response
-
end
-
end
-
-
1
private
-
-
1
def update parameters
-
2
id, attributes = parameters.to_h.values_at :id, :playlist
-
2
repository.update_with_items id, attributes, attributes[:items]
-
2
repository.with_items.by_pk(id).one
-
end
-
-
1
def unprocessable_content parameters, response
-
1
payload = problem[
-
type: "/problem_details#playlist_payload",
-
status: :unprocessable_content,
-
detail: "Validation failed.",
-
instance: "/api/playlists",
-
extensions: {errors: parameters.errors.to_h}
-
]
-
-
1
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Playlists
-
# The show action.
-
1
class Show < Base
-
1
include Deps[repository: "repositories.playlist"]
-
1
include Initable[serializer: Serializers::Playlist]
-
-
1
def handle request, response
-
2
playlist = repository.with_items.by_pk(request.params[:id]).one
-
-
2
then: 1
response.body = if playlist
-
1
{data: serializer.new(playlist).to_h}.to_json
-
else: 1
else
-
1
problem[status: :not_found].to_json
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Screens
-
# The create action.
-
1
class Create < Base
-
1
include Deps[
-
"aspects.screens.upserter",
-
repository: "repositories.screen",
-
playlist_item_repository: "repositories.playlist_item"
-
]
-
-
1
include Initable[serializer: Serializers::Screen]
-
-
1
using Refines::Actions::Response
-
-
1
params do
-
1
required(:screen).filled(:hash) do
-
1
optional(:playlist_id).filled :integer
-
1
required(:model_id).filled :integer
-
1
required(:label).filled :string
-
1
required(:name).filled :string
-
1
optional(:mode).filled :string
-
1
optional(:content).filled :string
-
1
optional(:uri).filled :string
-
1
optional(:preprocessed).filled :bool
-
end
-
end
-
-
1
def handle request, response
-
9
parameters = request.params
-
-
9
then: 7
if parameters.valid?
-
7
save parameters, response
-
else: 2
else
-
2
unprocessable_content_for_parameters parameters.errors.to_h, response
-
end
-
end
-
-
1
private
-
-
1
def save parameters, response
-
13
result = find(parameters).bind { upserter.call(**parameters[:screen]) }
-
-
7
case result
-
in: 4
in Success(screen)
-
4
create_playlist_item parameters.dig(:screen, :playlist_id), screen
-
4
else: 3
response.body = {data: serializer.new(screen).to_h}.to_json
-
3
else unprocessable_content_for_creation result, response
-
end
-
end
-
-
1
def find parameters
-
7
model_id, name = parameters[:screen].to_h.values_at :model_id, :name
-
-
7
else: 1
then: 6
return Success() unless repository.find_by(model_id:, name:)
-
-
1
Failure "Screen exists with name (#{name.inspect}) and model ID (#{model_id})."
-
end
-
-
1
def create_playlist_item playlist_id, screen
-
4
else: 1
then: 3
return unless playlist_id
-
-
1
playlist_item_repository.create_with_position playlist_id:, screen_id: screen.id
-
end
-
-
1
def unprocessable_content_for_parameters errors, response
-
2
payload = problem[
-
type: "/problem_details#screen_payload",
-
status: :unprocessable_content,
-
detail: "Validation failed.",
-
instance: "/api/screens",
-
extensions: {errors:}
-
]
-
-
2
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
-
1
def unprocessable_content_for_creation result, response
-
3
payload = problem[
-
type: "/problem_details#screen_payload",
-
status: :unprocessable_content,
-
detail: result.failure,
-
instance: "/api/screens"
-
]
-
-
3
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Screens
-
# The delete action.
-
1
class Delete < Base
-
1
include Deps[:settings, repository: "repositories.screen"]
-
1
include Initable[serializer: Serializers::Screen]
-
-
1
using Refines::Actions::Response
-
-
1
def handle request, response
-
2
repository.find(request.params[:id]).then do |screen|
-
2
then: 1
else: 1
screen ? success(screen, response) : failure(response)
-
end
-
end
-
-
1
private
-
-
1
def success screen, response
-
1
repository.delete screen.id
-
1
response.body = {data: serializer.new(screen).to_h}.to_json
-
end
-
-
1
def failure response
-
1
payload = problem[status: :not_found]
-
1
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Screens
-
# The index action.
-
1
class Index < Base
-
1
include Deps[repository: "repositories.screen"]
-
1
include Initable[serializer: Serializers::Screen]
-
-
1
def handle(*, response) = response.body = {data:}.to_json
-
-
1
private
-
-
2
def data = repository.all.map { serializer.new(it).to_h }
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Screens
-
# The patch action.
-
# :reek:DataClump
-
1
class Patch < Base
-
1
include Deps["aspects.screens.upserter", repository: "repositories.screen"]
-
1
include Initable[serializer: Serializers::Screen]
-
-
1
using Refines::Actions::Response
-
-
1
params do
-
1
required(:id).filled(:integer)
-
-
1
required(:screen).filled(:hash) do
-
1
optional(:model_id).filled :integer
-
1
optional(:label).filled :string
-
1
optional(:name).filled :string
-
1
optional(:mode).filled :string
-
1
optional(:content).filled :string
-
1
optional(:uri).filled :string
-
1
optional(:preprocessed).filled :bool
-
end
-
end
-
-
1
def handle request, response
-
5
parameters = request.params
-
-
5
then: 4
if parameters.valid?
-
4
save parameters, response
-
else: 1
else
-
1
unprocessable_content_for_parameters parameters.errors.to_h, response
-
end
-
end
-
-
1
private
-
-
1
def save parameters, response
-
7
result = find(parameters).bind { |record| update record, parameters }
-
-
4
case result
-
in: 2
in Success(screen)
-
2
else: 2
response.body = {data: serializer.new(screen).to_h}.to_json
-
2
else unprocessable_content_for_creation result, response
-
end
-
end
-
-
1
def find parameters
-
4
id = parameters[:id]
-
4
record = repository.find id
-
-
4
then: 3
else: 1
record ? Success(record) : Failure("Unable to find screen: #{id}.")
-
end
-
-
1
def update record, parameters
-
3
upserter.call(**record.to_h.slice(:model_id, :name, :label), **parameters[:screen])
-
end
-
-
1
def unprocessable_content_for_parameters errors, response
-
1
payload = problem[
-
type: "/problem_details#screen_payload",
-
status: :unprocessable_content,
-
detail: "Validation failed.",
-
instance: "/api/screens",
-
extensions: {errors:}
-
]
-
-
1
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
-
1
def unprocessable_content_for_creation result, response
-
2
payload = problem[
-
type: "/problem_details#screen_payload",
-
status: :unprocessable_content,
-
detail: result.failure,
-
instance: "/api/screens"
-
]
-
-
2
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module API
-
1
module Setup
-
# The show action.
-
1
class Show < Base
-
1
include Deps[
-
"aspects.devices.provisioner",
-
firmware_parser: "aspects.firmware.headers.parser",
-
model_repository: "repositories.model"
-
]
-
1
include Initable[payload: Aspects::Firmware::Models::Setup]
-
-
1
using Refines::Actions::Response
-
-
1
def handle request, response
-
6
in: 3
case firmware_parser.call request.env
-
3
in: 3
in Success(model) then create model, response
-
3
in Failure(result) then unprocessable_content result.errors.to_h, response
-
skipped
# :nocov:
-
skipped
# :nocov:
-
end
-
end
-
-
1
protected
-
-
1
def authorize(*) = nil
-
-
1
private
-
-
1
def create model, response
-
3
firmware_version, mac_address, model_name = model.to_h.values_at :firmware_version,
-
:mac_address,
-
:model_name
-
-
3
provisioner.call(model_id: find_model_id(model_name), mac_address:, firmware_version:)
-
2
.either -> device { render_success device, response },
-
1
-> error { not_found error, response }
-
end
-
-
4
then: 2
else: 1
def find_model_id(name) = model_repository.find_by(name:).then { it.id if it }
-
-
1
def render_success device, response
-
2
response.body = payload.for(device).to_json
-
end
-
-
1
def not_found error, response
-
1
payload = problem[
-
type: "/problem_details#device_setup",
-
status: __method__,
-
detail: error,
-
instance: "/api/setup"
-
]
-
-
1
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
-
1
def unprocessable_content errors, response
-
3
payload = problem[
-
type: "/problem_details#device_setup",
-
status: __method__,
-
detail: "Invalid request headers.",
-
instance: "/api/setup",
-
extensions: {errors:}
-
]
-
-
3
response.with body: payload.to_json, format: :problem_details, status: payload.status
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Bulk
-
1
module Devices
-
1
module Logs
-
# The delete action.
-
1
class Delete < Action
-
1
include Deps[repository: "repositories.device_log"]
-
-
2
params { required(:device_id).filled :integer }
-
-
1
def handle request, response
-
2
parameters = request.params
-
-
2
else: 1
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
1
repository.delete_all_by_device parameters[:device_id]
-
1
response.render view, layout: false
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Bulk
-
1
module Firmware
-
# The delete action.
-
1
class Delete < Action
-
1
include Deps[repository: "repositories.firmware"]
-
-
1
def handle _request, response
-
2
repository.delete_all
-
2
response.render view, layout: false
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Dashboard
-
# The show action.
-
1
class Show < Action
-
1
include Deps[:settings, firmware_repository: "repositories.firmware"]
-
1
include Initable[ip_finder: proc { Terminus::IPFinder.new }]
-
-
1
def handle _request, response
-
55
response.render view,
-
api_uri: settings.api_uri,
-
firmware: firmware_repository.latest,
-
ip_addresses: ip_finder.all
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Designer
-
# The create action.
-
1
class Create < Action
-
1
include Deps[
-
:htmx,
-
"aspects.screens.upserter",
-
model_repository: "repositories.model",
-
screen_repository: "repositories.screen",
-
show_view: "views.designer.show"
-
]
-
-
1
using Refines::Actions::Response
-
-
1
params do
-
1
required(:template).filled(:hash) do
-
1
required(:name).filled :string
-
1
required(:label).filled :string
-
1
required(:content).filled :string
-
end
-
end
-
-
1
def handle request, response
-
6
parameters = request.params
-
-
6
else: 5
then: 1
halt 422 unless parameters.valid?
-
-
5
then: 4
if htmx.request? request.env, :request, "true"
-
4
render_text parameters[:template], response
-
else: 1
else
-
1
response.render show_view, id: Time.new.utc.to_i
-
end
-
end
-
-
1
private
-
-
1
def render_text template, response
-
4
name, label, content = template.values_at :name, :label, :content
-
-
4
rebuild_screen name, label, content
-
4
response.with body: content.strip, status: 201
-
end
-
-
1
def rebuild_screen name, label, content
-
8
then: 1
else: 3
screen_repository.find_by(name:).then { screen_repository.delete it.id if it }
-
4
upserter.call model_id: load_model.id, name:, label:, content:
-
end
-
-
# FIX: Use dynamic lookup once the UI support picking the correct model.
-
1
def load_model = model_repository.find_by name: "og_png"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Designer
-
# The show action.
-
1
class Show < Action
-
1
def handle(*, response) = response.render view
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Devices
-
# The create action.
-
1
class Create < Action
-
1
include Deps[
-
:htmx_layout,
-
"aspects.devices.provisioner",
-
repository: "repositories.device",
-
model_repository: "repositories.model",
-
playlist_repository: "repositories.playlist",
-
index_view: "views.devices.index"
-
]
-
-
1
contract Contracts::Devices::Create
-
-
1
def handle request, response
-
4
parameters = request.params
-
-
4
case provision parameters
-
in: 3
in Success
-
3
else: 1
response.render index_view, devices: repository.all, layout: htmx_layout.call(request)
-
1
else error response, parameters
-
end
-
end
-
-
1
private
-
-
1
def provision parameters
-
4
then: 3
else: 1
parameters.valid? ? provisioner.call(**parameters[:device]) : Failure
-
end
-
-
1
def error response, parameters
-
1
response.render view,
-
models: model_repository.all,
-
playlists: playlist_repository.all,
-
device: nil,
-
fields: parameters[:device],
-
errors: parameters.errors[:device],
-
layout: false
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Devices
-
# The delete action.
-
1
class Delete < Action
-
1
include Deps[repository: "repositories.device"]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
else: 2
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
2
repository.delete parameters[:id]
-
2
response.body = ""
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Devices
-
# The edit action.
-
1
class Edit < Action
-
1
include Deps[
-
:htmx_layout,
-
repository: "repositories.device",
-
model_repository: "repositories.model",
-
playlist_repository: "repositories.playlist"
-
]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
4
parameters = request.params
-
-
4
else: 3
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
3
response.render view, **view_settings(request, parameters)
-
end
-
-
1
private
-
-
1
def view_settings request, parameters
-
{
-
3
models: model_repository.all,
-
playlists: playlist_repository.all,
-
device: repository.find(parameters[:id]),
-
layout: htmx_layout.call(request)
-
}
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Devices
-
# The index action.
-
1
class Index < Action
-
1
include Deps[:htmx, repository: "repositories.device"]
-
-
1
def handle request, response
-
9
query = request.params[:query].to_s
-
9
devices = load query
-
-
9
then: 3
if htmx.request? request.env, :trigger, "search"
-
3
add_htmx_headers response, query
-
3
response.render view, devices:, query:, layout: false
-
else: 6
else
-
6
response.render view, devices:, query:
-
end
-
end
-
-
1
private
-
-
1
def load query
-
9
then: 5
else: 4
query.empty? ? repository.all : repository.search(:label, query)
-
end
-
-
1
def add_htmx_headers response, query
-
3
then: 1
else: 2
return if query.empty?
-
-
2
htmx.response! response.headers, push_url: routes.path(:devices, query:)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Devices
-
1
module Logs
-
# The delete action.
-
1
class Delete < Action
-
1
include Deps[repository: "repositories.device_log"]
-
-
1
params do
-
1
required(:device_id).filled :integer
-
1
required(:id).filled :integer
-
end
-
-
1
def handle request, response
-
2
parameters = request.params
-
-
2
else: 1
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
1
repository.delete_by_device(*parameters.to_h.values_at(:device_id, :id))
-
1
response.body = ""
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Devices
-
1
module Logs
-
# The index action.
-
# :reek:DataClump
-
1
class Index < Action
-
1
include Deps[
-
:htmx,
-
device_repository: "repositories.device",
-
repository: "repositories.device_log"
-
]
-
-
1
params do
-
1
required(:device_id).filled :integer
-
1
optional(:query).maybe :string
-
end
-
-
1
def handle request, response
-
7
parameters = request.params
-
-
7
else: 6
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
6
device = device_repository.find parameters[:device_id]
-
-
6
then: 2
if htmx.request? request.env, :trigger, "search"
-
2
render_search_results parameters, device, response
-
else: 4
else
-
4
render_all parameters, device, response
-
end
-
end
-
-
1
private
-
-
1
def render_search_results parameters, device, response
-
2
query = parameters[:query].to_s
-
2
add_htmx_headers response, device, query
-
-
2
response.render view, device:, logs: load(device.id, query), query:, layout: false
-
end
-
-
1
def render_all parameters, device, response
-
4
query = parameters[:query].to_s
-
4
response.render view, device:, logs: load(device.id, query), query:
-
end
-
-
1
def load device_id, query
-
6
then: 4
else: 2
return repository.where(device_id:) if query.empty?
-
-
2
repository.search :message, query, device_id:
-
end
-
-
1
def add_htmx_headers response, device, query
-
2
then: 1
else: 1
return if query.empty?
-
-
1
htmx.response! response.headers,
-
push_url: routes.path(:device_logs, device_id: device.id, query:)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Devices
-
1
module Logs
-
# The show action.
-
1
class Show < Action
-
1
include Deps[
-
device_repository: "repositories.device",
-
repository: "repositories.device_log"
-
]
-
-
1
params do
-
1
required(:device_id).filled :integer
-
1
required(:id).filled :integer
-
end
-
-
1
def handle request, response
-
1
parameters = request.params
-
-
1
device = device_repository.find parameters[:device_id]
-
1
log = repository.find parameters[:id]
-
-
1
response.render view, device:, log:
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Devices
-
# The new action.
-
1
class New < Action
-
1
include Deps[
-
:htmx_layout,
-
"aspects.devices.defaulter",
-
model_repository: "repositories.model",
-
playlist_repository: "repositories.playlist"
-
]
-
-
1
def handle request, response
-
3
response.render view,
-
models: model_repository.all,
-
playlists: playlist_repository.all,
-
fields: defaulter.call,
-
layout: htmx_layout.call(request)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Devices
-
# The show action.
-
1
class Show < Action
-
1
include Deps[:htmx_layout, repository: "repositories.device"]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
4
parameters = request.params
-
-
4
else: 3
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
3
response.render view,
-
device: repository.find(parameters[:id]),
-
layout: htmx_layout.call(request)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Devices
-
# The update action.
-
1
class Update < Action
-
1
include Deps[
-
repository: "repositories.device",
-
model_repository: "repositories.model",
-
playlist_repository: "repositories.playlist",
-
show_view: "views.devices.show"
-
]
-
-
1
contract Contracts::Devices::Update
-
-
1
def handle request, response
-
3
parameters = request.params
-
3
device = repository.find parameters[:id]
-
-
3
else: 2
then: 1
halt :unprocessable_content unless device
-
-
2
then: 1
if parameters.valid?
-
1
save device, parameters, response
-
else: 1
else
-
1
error device, parameters, response
-
end
-
end
-
-
1
private
-
-
1
def save device, parameters, response
-
1
id = device.id
-
1
repository.update id, **parameters[:device]
-
-
1
response.render show_view, device: repository.find(id), layout: false
-
end
-
-
1
def error device, parameters, response
-
1
response.render view,
-
models: model_repository.all,
-
playlists: playlist_repository.all,
-
device:,
-
fields: parameters[:device],
-
errors: parameters.errors[:device],
-
layout: false
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
1
module Build
-
# The create action.
-
1
class Create < Action
-
1
include Deps[:htmx, repository: "repositories.extension"]
-
1
include Initable[job: Jobs::Batches::Extension]
-
-
2
params { required(:extension_id).filled :integer }
-
-
1
def handle request, response
-
3
extension = repository.find request.params[:extension_id]
-
3
enqueue extension, response
-
end
-
-
1
private
-
-
1
def enqueue extension, response
-
3
job.perform_async extension.id
-
-
3
response.status = 202
-
3
response.render view, extension:, layout: false
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
1
module Clone
-
# The create action.
-
1
class Create < Action
-
1
include Deps["aspects.extensions.cloner", repository: "repositories.extension"]
-
-
1
using Refinements::Hash
-
-
1
params do
-
1
required(:extension_id).filled :integer
-
1
required(:extension).filled Schemas::Extensions::Upsert
-
end
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
then: 2
if parameters.valid?
-
2
save parameters, response
-
else: 1
else
-
1
error parameters, parameters.errors[:extension], response
-
end
-
end
-
-
1
private
-
-
1
def save parameters, response
-
2
in: 1
case cloner.call parameters[:extension_id], **parameters[:extension]
-
1
in: 1
in Success then response.redirect_to routes.path(:extensions)
-
1
in Failure(errors) then error parameters, errors, response
-
skipped
# :nocov:
-
skipped
# :nocov:
-
end
-
end
-
-
1
def error parameters, errors, response
-
2
fields = parameters[:extension].transform_with!(
-
2
start_at: -> value { value.strftime("%Y-%m-%dT%H:%M:%S") }
-
)
-
-
2
response.render view,
-
extension: repository.find(parameters[:extension_id]),
-
fields:,
-
errors:
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
1
module Clone
-
# The new action.
-
1
class New < Hanami::Action
-
1
include Deps[repository: "repositories.extension"]
-
-
2
params { required(:extension_id).filled :integer }
-
-
1
def handle request, response
-
1
extension = repository.find request.params[:extension_id]
-
1
fields = {label: "#{extension.label} Clone", name: "#{extension.name}_clone"}
-
-
1
response.render view, extension:, fields:
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
# The create action.
-
1
class Create < Action
-
1
include Deps[
-
:htmx_layout,
-
"aspects.jobs.schedule",
-
repository: "repositories.extension",
-
index_view: "views.extensions.index"
-
]
-
-
1
using Refinements::Hash
-
-
1
contract Contracts::Extensions::Create
-
-
1
def handle request, response
-
4
parameters = request.params
-
-
4
then: 3
if parameters.valid?
-
3
save parameters
-
3
response.render index_view,
-
extensions: repository.all,
-
layout: htmx_layout.call(request)
-
else: 1
else
-
1
error response, parameters
-
end
-
end
-
-
1
private
-
-
1
def save parameters
-
3
attributes = parameters[:extension]
-
3
model_ids, device_ids = attributes.values_at :model_ids, :device_ids
-
3
extension = repository.create_with_models attributes, Array(model_ids)
-
-
3
repository.update_with_devices extension.id, {}, Array(device_ids)
-
3
schedule.upsert(*extension.to_schedule)
-
end
-
-
1
def error response, parameters
-
1
fields = parameters[:extension].transform_with!(
-
1
start_at: -> value { value.strftime("%Y-%m-%dT%H:%M:%S") }
-
)
-
-
1
response.render view, fields:, errors: parameters.errors[:extension], layout: false
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
# The delete action.
-
1
class Delete < Action
-
1
include Deps["aspects.jobs.schedule", repository: "repositories.extension"]
-
-
1
def handle request, response
-
3
extension = repository.find request.params[:id]
-
-
3
else: 2
then: 1
halt :unprocessable_content unless extension
-
-
2
repository.delete extension.id
-
2
schedule.delete extension.screen_name
-
2
response.body = ""
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
# The edit action.
-
1
class Edit < Action
-
1
include Deps[repository: "repositories.extension"]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
6
parameters = request.params
-
-
6
else: 5
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
5
response.render view, extension: repository.find(parameters[:id])
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "initable"
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
1
module Exchanges
-
# The create action.
-
1
class Create < Action
-
1
include Deps[
-
:htmx,
-
extension_repository: "repositories.extension",
-
repository: "repositories.extension_exchange"
-
]
-
1
include Initable[job: Jobs::Extensions::ExchangeRefresh]
-
-
1
contract Contracts::Extensions::Exchanges::Create
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
then: 2
if parameters.valid?
-
2
save parameters, response
-
else: 1
else
-
1
error parameters, response
-
end
-
end
-
-
1
private
-
-
1
def save parameters, response
-
2
extension_id, exchange = parameters.to_h.values_at :extension_id, :exchange
-
2
job.perform_async repository.create(extension_id:, **exchange).id
-
-
2
response.redirect_to routes.path(
-
:extension_exchanges,
-
extension_id: parameters[:extension_id]
-
)
-
end
-
-
1
def error parameters, response
-
1
extension_id, fields = parameters.to_h.values_at :extension_id, :exchange
-
-
1
response.render view,
-
extension: extension_repository.find(extension_id),
-
fields:,
-
errors: parameters.errors[:exchange]
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
1
module Exchanges
-
# The delete action.
-
1
class Delete < Action
-
1
include Deps[repository: "repositories.extension_exchange"]
-
-
1
params do
-
1
required(:extension_id).filled :integer
-
1
required(:id).filled :integer
-
end
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
else: 2
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
2
record = repository.find_by(**parameters)
-
-
2
repository.delete record.id
-
2
response.body = ""
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
1
module Exchanges
-
# The edit action.
-
1
class Edit < Action
-
1
include Deps[
-
:htmx_layout,
-
extension_repository: "repositories.extension",
-
repository: "repositories.extension_exchange"
-
]
-
-
1
params do
-
1
required(:extension_id).filled :integer
-
1
required(:id).filled :integer
-
end
-
-
1
def handle request, response
-
4
parameters = request.params
-
-
4
else: 3
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
3
response.render view, **view_settings(request, parameters)
-
end
-
-
1
private
-
-
1
def view_settings request, parameters
-
{
-
3
extension: extension_repository.find(parameters[:extension_id]),
-
exchange: repository.find_by(**parameters),
-
layout: htmx_layout.call(request)
-
}
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
1
module Exchanges
-
# The index action.
-
1
class Index < Action
-
1
include Deps[
-
:htmx,
-
extension_repository: "repositories.extension",
-
repository: "repositories.extension_exchange"
-
]
-
-
1
def handle request, response
-
4
response.render view, **view_settings(request)
-
end
-
-
1
def view_settings request
-
4
parameters = request.params
-
4
extension = extension_repository.find parameters[:extension_id]
-
-
4
{extension:, exchanges: repository.where(extension_id: extension.id)}
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
1
module Exchanges
-
# The new action.
-
1
class New < Action
-
1
include Deps[:htmx_layout, extension_repository: "repositories.extension"]
-
-
2
params { required(:extension_id).filled :integer }
-
-
1
def handle request, response
-
4
parameters = request.params
-
-
4
else: 3
then: 1
halt 422 unless parameters.valid?
-
-
3
response.render view, **view_settings(request, parameters)
-
end
-
-
1
private
-
-
1
def view_settings request, parameters
-
{
-
3
extension: extension_repository.find(parameters[:extension_id]),
-
fields: {verb: "get"},
-
layout: htmx_layout.call(request)
-
}
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "initable"
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
1
module Exchanges
-
# The update action.
-
1
class Update < Action
-
1
include Deps[
-
extension_repository: "repositories.extension",
-
repository: "repositories.extension_exchange"
-
]
-
1
include Initable[job: Jobs::Extensions::ExchangeRefresh]
-
-
1
contract Contracts::Extensions::Exchanges::Update
-
-
1
def handle request, response
-
2
parameters = request.params
-
2
extension_id, id = parameters.to_h.values_at :extension_id, :id
-
2
exchange = repository.find_by(extension_id:, id:)
-
-
2
then: 1
if parameters.valid?
-
1
save exchange, parameters, response
-
else: 1
else
-
1
error exchange, parameters, response
-
end
-
end
-
-
1
private
-
-
1
def save exchange, parameters, response
-
1
id = exchange.id
-
-
1
repository.update id, **parameters[:exchange]
-
1
job.perform_async id
-
-
1
response.redirect_to routes.path(
-
:extension_exchanges,
-
extension_id: exchange.extension_id
-
)
-
end
-
-
1
def error exchange, parameters, response
-
1
response.render view,
-
extension: extension_repository.find(exchange.extension_id),
-
exchange:,
-
fields: parameters[:exchange],
-
errors: parameters.errors[:exchange]
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
1
module Export
-
# The show action.
-
1
class Show < Action
-
1
config.formats.accept :yml
-
-
1
include Deps["aspects.extensions.exporter", repository: "repositories.extension"]
-
-
1
using Refinements::Hash
-
1
using Refines::Actions::Response
-
-
2
params { required(:extension_id).filled :integer }
-
-
1
def handle request, response
-
4
parameters = request.params
-
-
4
else: 3
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
3
extension = repository.find parameters[:extension_id]
-
-
3
in: 2
case exporter.call extension
-
2
in: 1
in Success(body) then response.with body: body.deep_stringify_keys!.to_yaml
-
1
in Failure(message) then response.with body: {"error" => message}.to_yaml
-
skipped
# :nocov:
-
skipped
# :nocov:
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
1
module Gallery
-
# The index action.
-
1
class Index < Action
-
1
include Deps[:htmx, trmnl_api: :trmnl_api_recipes]
-
1
include Initable[empty_recipe: proc { TRMNL::API::Models::Recipe.empty }]
-
-
1
params do
-
1
optional(:query).filled :string
-
1
optional(:page).filled :integer
-
end
-
-
1
def handle request, response
-
9
parameters = request.params
-
-
17
load(parameters).either -> recipe { render request, recipe, response },
-
1
-> message { render_error parameters, message, response }
-
end
-
-
1
private
-
-
1
def load parameters
-
9
in: 1
case parameters
-
1
in: 5
in query:, page: then trmnl_api.recipes(search: query, page:)
-
5
in: 1
in query: then trmnl_api.recipes search: query
-
1
else: 2
in page: then trmnl_api.recipes(page:)
-
2
else trmnl_api.recipes
-
end
-
end
-
-
1
def render request, recipe, response
-
8
query, page = request.params.to_h.values_at :query, :page
-
-
8
then: 2
if htmx.request(**request.env).request?
-
2
htmx.response! response.headers,
-
push_url: routes.path(:extensions_gallery, query:, page:)
-
2
response.render view, recipe:, query:, page:, layout: false
-
else: 6
else
-
6
response.render view, recipe:, query:, page:
-
end
-
end
-
-
1
def render_error parameters, message, response
-
1
response.flash.now[:alert] = message
-
1
response.render view, recipe: empty_recipe, **parameters.to_h.slice(:query, :page)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
# The index action.
-
1
class Index < Action
-
1
include Deps[:htmx, repository: "repositories.extension"]
-
-
1
def handle request, response
-
10
query = request.params[:query].to_s
-
10
extensions = load query
-
-
10
then: 3
if htmx.request? request.env, :trigger, "search"
-
3
add_htmx_headers response, query
-
3
response.render view, extensions:, query:, layout: false
-
else: 7
else
-
7
response.render view, extensions:, query:
-
end
-
end
-
-
1
private
-
-
11
then: 6
else: 4
def load(query) = query.empty? ? repository.all : repository.search(:label, query)
-
-
1
def add_htmx_headers response, query
-
3
then: 1
else: 2
return if query.empty?
-
-
2
htmx.response! response.headers, push_url: routes.path(:extensions, query:)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
# The new action.
-
1
class New < Action
-
1
include Deps[:htmx_layout]
-
-
1
def initialize(defaults: Aspects::Extensions::DEFAULTS, **)
-
3
@defaults = defaults
-
3
super(**)
-
end
-
-
1
def handle request, response
-
3
response.render view, fields: defaults, layout: htmx_layout.call(request)
-
end
-
-
1
private
-
-
1
attr_reader :defaults
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
1
module Preview
-
# The show action.
-
1
class Show < Action
-
1
include Deps[
-
"aspects.extensions.renderer",
-
repository: "repositories.extension",
-
view: "views.extensions.dynamic"
-
]
-
-
1
params do
-
1
required(:extension_id).filled :integer
-
1
required(:model_id).filled :integer
-
1
required(:device_id).maybe :integer
-
end
-
-
1
def handle request, response
-
8
id, model_id, device_id = request.params.to_h.values_at :extension_id,
-
:model_id,
-
:device_id
-
8
extension = repository.find id
-
-
8
else: 7
then: 1
halt :not_found unless extension
-
-
7
response.render view, content: content_for(extension, model_id, device_id)
-
end
-
-
1
private
-
-
1
def content_for extension, model_id, device_id
-
7
in: 5
case renderer.call(extension, model_id:, device_id:)
-
5
in: 1
in Success(content) then content
-
1
else: 1
in Failure(message) then message
-
1
else "Unable to render body for extension: #{extension.id}."
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
1
module Sensors
-
# The index action.
-
1
class Index < Action
-
1
include Deps[
-
repository: "repositories.extension",
-
sensor_repository: "repositories.device_sensor"
-
]
-
1
include Initable[json_formatter: Aspects::JSONFormatter]
-
-
1
using Refinements::Hash
-
-
2
params { required(:extension_id).filled :integer }
-
-
1
def handle request, response
-
10
extension = repository.find request.params[:extension_id]
-
-
10
else: 9
then: 1
halt :not_found unless extension
-
-
9
content = load_content extension
-
-
9
response.render view, content: json_formatter.call(content), layout: false
-
end
-
-
1
def load_content extension
-
9
device_ids = extension.devices.map(&:id)
-
9
sensors = sensor_repository.limited_where device_id: device_ids
-
9
{sensors: sensors.map(&:liquid_attributes)}
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
1
module Sources
-
# The index action.
-
1
class Index < Action
-
1
include Deps[:htmx_layout, repository: "repositories.extension_exchange"]
-
-
2
params { required(:extension_id).filled :integer }
-
-
1
def initialize(
-
coalescer: Aspects::Extensions::Exchanges::Coalescer,
-
json_formatter: Aspects::JSONFormatter,
-
**
-
)
-
10
@coalescer = coalescer
-
10
@json_formatter = json_formatter
-
10
super(**)
-
end
-
-
1
def handle request, response
-
10
exchanges = repository.where extension_id: request.params[:extension_id]
-
10
content = json_formatter.call coalescer.call(exchanges)
-
-
10
response.render view, content:, layout: htmx_layout.call(request)
-
end
-
-
1
private
-
-
1
attr_reader :coalescer, :json_formatter
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Actions
-
1
module Extensions
-
# The update action.
-
1
class Update < Action
-
1
include Deps["aspects.jobs.schedule", repository: "repositories.extension"]
-
-
1
using Refinements::Hash
-
-
1
contract Contracts::Extensions::Update
-
-
1
def handle request, response
-
3
parameters = request.params
-
3
extension = repository.find parameters[:id]
-
-
3
else: 2
then: 1
halt :unprocessable_content unless extension
-
-
2
then: 1
if parameters.valid?
-
1
render extension, parameters, response
-
else: 1
else
-
1
error extension, parameters, response
-
end
-
end
-
-
1
private
-
-
1
def render extension, parameters, response
-
1
update extension, parameters[:extension]
-
-
1
response.flash[:notice] = "Changes saved."
-
1
response.redirect_to routes.path(:extension_edit, id: extension.id)
-
end
-
-
1
def update extension, attributes
-
1
id = extension.id
-
1
model_ids, device_ids = attributes.values_at :model_ids, :device_ids
-
-
1
repository.update_with_devices id, attributes, Array(device_ids)
-
-
1
extension = repository.update_with_models id, attributes, Array(model_ids)
-
-
1
schedule.upsert(*extension.to_schedule, old_name: extension.screen_name)
-
end
-
-
1
def error extension, parameters, response
-
1
fields = parameters[:extension].transform_with!(
-
1
start_at: -> value { value.strftime("%Y-%m-%dT%H:%M:%S") }
-
)
-
-
1
response.render view, extension:, fields:, errors: parameters.errors[:extension]
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Firmware
-
# The create action.
-
1
class Create < Action
-
1
include Deps[
-
:htmx,
-
repository: "repositories.firmware",
-
index_view: "views.firmware.index"
-
]
-
-
1
params do
-
1
required(:firmware).filled :hash do
-
1
required(:version).filled Types::Version
-
1
required(:kind).filled :string
-
1
required(:attachment).filled :hash
-
end
-
end
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
then: 1
if parameters.valid?
-
1
save parameters[:firmware]
-
1
response.render index_view, firmware: repository.all
-
else: 2
else
-
2
error response, parameters
-
end
-
end
-
-
1
private
-
-
# :reek:FeatureEnvy
-
1
def save attributes
-
1
attachment = attributes.delete :attachment
-
1
record = repository.create attributes
-
-
1
record.upload attachment[:tempfile], metadata: {"filename" => "#{record.version}.bin"}
-
1
repository.update record.id, attachment_data: record.attachment_attributes
-
end
-
-
1
def error response, parameters
-
2
response.render view,
-
firmware: nil,
-
fields: parameters[:firmware],
-
errors: parameters.errors[:firmware]
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Firmware
-
# The delete action.
-
1
class Delete < Action
-
1
include Deps[repository: "repositories.firmware"]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
5
parameters = request.params
-
-
5
else: 4
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
4
repository.delete parameters[:id]
-
4
response.body = ""
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Firmware
-
# The edit action.
-
1
class Edit < Action
-
1
include Deps[:htmx_layout, repository: "repositories.firmware"]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
5
parameters = request.params
-
-
5
else: 4
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
4
response.render view,
-
firmware: repository.find(parameters[:id]),
-
layout: htmx_layout.call(request)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Firmware
-
# The index action.
-
1
class Index < Action
-
1
include Deps[:htmx, repository: "repositories.firmware"]
-
-
1
def handle request, response
-
11
query = request.params[:query]
-
11
firmware = load query
-
-
11
then: 3
if htmx.request? request.env, :trigger, "search"
-
3
add_htmx_headers response, query
-
3
response.render view, firmware:, query:, layout: false
-
else: 8
else
-
8
response.render view, firmware:, query:
-
end
-
end
-
-
1
private
-
-
12
then: 5
else: 6
def load(query) = query ? repository.search(:version, query) : repository.all
-
-
1
def add_htmx_headers response, query
-
3
htmx.response! response.headers, push_url: routes.path(:firmware, query:)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Firmware
-
# The new action.
-
1
class New < Action
-
1
include Deps[:htmx_layout]
-
-
1
def handle request, response
-
3
response.render view, fields: {kind: "terminus"}, layout: htmx_layout.call(request)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Firmware
-
# The show action.
-
1
class Show < Action
-
1
include Deps[:htmx_layout, repository: "repositories.firmware"]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
5
parameters = request.params
-
-
5
else: 4
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
4
response.render view,
-
firmware: repository.find(parameters[:id]),
-
layout: htmx_layout.call(request)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Firmware
-
# The update action.
-
1
class Update < Action
-
1
include Deps[repository: "repositories.firmware"]
-
-
1
params do
-
1
required(:id).filled :integer
-
-
1
required(:firmware).filled :hash do
-
1
required(:version).filled Types::Version
-
1
required(:kind).filled :string
-
1
optional(:attachment).filled :hash
-
end
-
end
-
-
1
def handle request, response
-
4
parameters = request.params
-
4
record = repository.find parameters[:id]
-
-
4
else: 3
then: 1
halt :unprocessable_content unless record
-
-
3
then: 2
if parameters.valid?
-
2
save record, parameters, response
-
else: 1
else
-
1
error record, parameters, response
-
end
-
end
-
-
1
private
-
-
# :reek:TooManyStatements
-
1
def save record, parameters, response
-
2
id = record.id
-
2
attributes = parameters[:firmware]
-
2
attachment = attributes.delete :attachment
-
-
2
repository.update id, **attributes
-
2
attach record, attachment
-
2
response.redirect_to routes.path(:firmware_show, id:)
-
end
-
-
# :reek:FeatureEnvy
-
1
def attach record, attachment
-
2
else: 1
then: 1
return unless attachment
-
-
1
record.replace attachment[:tempfile], metadata: {"filename" => "#{record.version}.bin"}
-
1
repository.update record.id, attachment_data: record.attachment_attributes
-
end
-
-
1
def error record, parameters, response
-
1
response.render view,
-
firmware: record,
-
fields: parameters[:firmware],
-
errors: parameters.errors[:firmware]
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Models
-
1
module Clone
-
# The create action.
-
1
class Create < Action
-
1
include Deps["aspects.models.cloner", repository: "repositories.model"]
-
-
1
contract Contracts::Models::Clone
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
then: 2
if parameters.valid?
-
2
clone parameters, response
-
else: 1
else
-
1
render_form_error parameters, parameters.errors[:model], response
-
end
-
end
-
-
1
private
-
-
1
def clone parameters, response
-
2
in: 1
case cloner.call parameters[:model_id], **parameters[:model]
-
1
in: 1
in Success then response.redirect_to routes.path(:models)
-
1
in Failure(errors) then render_form_error parameters, errors, response
-
skipped
# :nocov:
-
skipped
# :nocov:
-
end
-
end
-
-
1
def render_form_error parameters, errors, response
-
2
id, fields = parameters.to_h.values_at :model_id, :model
-
2
response.render view, model: repository.find(id), fields:, errors:
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Models
-
1
module Clone
-
# The new action.
-
1
class New < Action
-
1
include Deps[repository: "repositories.model"]
-
-
1
def handle request, response
-
1
model = repository.find request.params[:model_id]
-
1
fields = {label: "#{model.label} Clone", name: "#{model.name}_clone"}
-
-
1
response.render view, model:, fields:
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Models
-
# The create action.
-
1
class Create < Action
-
1
include Deps[
-
:htmx_layout,
-
repository: "repositories.model",
-
index_view: "views.models.index"
-
]
-
-
1
contract Contracts::Models::Create
-
-
1
def handle request, response
-
4
parameters = request.params
-
-
4
then: 3
if parameters.valid?
-
3
repository.create parameters[:model]
-
3
response.render index_view, **view_settings(request)
-
else: 1
else
-
1
error response, parameters
-
end
-
end
-
-
1
private
-
-
1
def view_settings(request) = {models: repository.all, layout: htmx_layout.call(request)}
-
-
1
def error response, parameters
-
1
response.render view,
-
models: repository.all,
-
fields: parameters[:model],
-
errors: parameters.errors[:model],
-
layout: false
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Models
-
# The delete action.
-
1
class Delete < Action
-
1
include Deps[repository: "repositories.model"]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
else: 2
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
2
repository.delete parameters[:id]
-
2
response.body = ""
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Models
-
# The edit action.
-
1
class Edit < Action
-
1
include Deps[:htmx_layout, repository: "repositories.model"]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
4
parameters = request.params
-
-
4
else: 3
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
3
response.render view,
-
model: repository.find(parameters[:id]),
-
layout: htmx_layout.call(request)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Models
-
# The index action.
-
1
class Index < Action
-
1
include Deps[:htmx, repository: "repositories.model"]
-
-
1
def handle request, response
-
10
query = request.params[:query].to_s
-
10
models = load query
-
-
10
then: 3
if htmx.request? request.env, :trigger, "search"
-
3
add_htmx_headers response, query
-
3
response.render view, models:, query:, layout: false
-
else: 7
else
-
7
response.render view, models:, query:
-
end
-
end
-
-
1
private
-
-
11
then: 6
else: 4
def load(query) = query.empty? ? repository.all : repository.search(:label, query)
-
-
1
def add_htmx_headers response, query
-
3
then: 1
else: 2
return if query.empty?
-
-
2
htmx.response! response.headers, push_url: routes.path(:models, query:)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Models
-
# The new action.
-
1
class New < Action
-
1
include Deps[:htmx_layout]
-
-
1
def initialize(defaults: Aspects::Models::DEFAULTS, **)
-
3
@defaults = defaults
-
3
super(**)
-
end
-
-
1
def handle request, response
-
3
response.render view, fields: defaults, layout: htmx_layout.call(request)
-
end
-
-
1
private
-
-
1
attr_reader :defaults
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Models
-
# The show action.
-
1
class Show < Action
-
1
include Deps[:htmx_layout, repository: "repositories.model"]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
else: 2
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
2
response.render view,
-
model: repository.find(parameters[:id]),
-
layout: htmx_layout.call(request)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Models
-
# The update action.
-
1
class Update < Action
-
1
include Deps[repository: "repositories.model", show_view: "views.models.show"]
-
-
1
contract Contracts::Models::Update
-
-
1
def handle request, response
-
3
parameters = request.params
-
3
model = repository.find parameters[:id]
-
-
3
else: 2
then: 1
halt :unprocessable_content unless model
-
-
2
then: 1
if parameters.valid?
-
1
save model, parameters, response
-
else: 1
else
-
1
error model, parameters, response
-
end
-
end
-
-
1
private
-
-
1
def save model, parameters, response
-
1
id = model.id
-
1
repository.update id, **parameters[:model]
-
-
1
response.render show_view, model: repository.find(id), layout: false
-
end
-
-
1
def error model, parameters, response
-
1
response.render view,
-
model:,
-
fields: parameters[:model],
-
errors: parameters.errors[:model],
-
layout: false
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Playlists
-
1
module Clone
-
# The create action.
-
1
class Create < Action
-
1
include Deps["aspects.playlists.cloner", repository: "repositories.playlist"]
-
-
1
params do
-
1
required(:playlist_id).filled :integer
-
-
1
required(:playlist).hash do
-
1
required(:label).filled :string
-
1
required(:name).filled :string
-
1
required(:mode).filled :string
-
end
-
end
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
then: 2
if parameters.valid?
-
2
clone parameters, response
-
else: 1
else
-
1
render_form_error parameters, parameters.errors[:playlist], response
-
end
-
end
-
-
1
private
-
-
1
def clone parameters, response
-
2
in: 1
case cloner.call parameters[:playlist_id], **parameters[:playlist]
-
1
in: 1
in Success then response.redirect_to routes.path(:playlists)
-
1
in Failure(errors) then render_form_error parameters, errors, response
-
skipped
# :nocov:
-
skipped
# :nocov:
-
end
-
end
-
-
1
def render_form_error parameters, errors, response
-
2
id, fields = parameters.to_h.values_at :playlist_id, :playlist
-
2
response.render view, playlist: repository.find(id), fields:, errors:
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Playlists
-
1
module Clone
-
# The new action.
-
1
class New < Action
-
1
include Deps[repository: "repositories.playlist"]
-
-
1
def handle request, response
-
1
playlist = repository.find request.params[:playlist_id]
-
1
fields = {label: "#{playlist.label} Clone", name: "#{playlist.name}_clone"}
-
-
1
response.render view, playlist:, fields:
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Playlists
-
# The create action.
-
1
class Create < Action
-
1
include Deps[
-
:htmx_layout,
-
repository: "repositories.playlist",
-
index_view: "views.playlists.index"
-
]
-
-
1
params do
-
1
required(:playlist).hash do
-
1
required(:label).filled :string
-
1
required(:name).filled :string
-
1
required(:mode).filled :string
-
end
-
end
-
-
1
def handle request, response
-
4
parameters = request.params
-
-
4
then: 3
if parameters.valid?
-
3
repository.create parameters[:playlist]
-
3
response.render index_view, playlists: repository.all, layout: htmx_layout.call(request)
-
else: 1
else
-
1
error response, parameters
-
end
-
end
-
-
1
private
-
-
1
def error response, parameters
-
1
response.render view,
-
playlist: nil,
-
fields: parameters[:playlist],
-
errors: parameters.errors[:playlist],
-
layout: false
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Playlists
-
# The delete action.
-
1
class Delete < Action
-
1
include Deps[repository: "repositories.playlist"]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
else: 2
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
2
repository.delete parameters[:id]
-
2
response.body = ""
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Playlists
-
# The edit action.
-
1
class Edit < Action
-
1
include Deps[
-
:htmx_layout,
-
repository: "repositories.playlist",
-
item_repository: "repositories.playlist_item"
-
]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
4
parameters = request.params
-
-
4
else: 3
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
3
response.render view, **view_settings(request, parameters)
-
end
-
-
1
private
-
-
1
def view_settings request, parameters
-
3
playlist = repository.find parameters[:id]
-
-
{
-
3
playlist:,
-
items: item_repository.where(playlist_id: playlist.id),
-
layout: htmx_layout.call(request)
-
}
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Playlists
-
# The index action.
-
1
class Index < Action
-
1
include Deps[:htmx, repository: "repositories.playlist"]
-
-
1
def handle request, response
-
12
query = request.params[:query].to_s
-
12
playlists = load query
-
-
12
then: 3
if htmx.request? request.env, :trigger, "search"
-
3
add_htmx_headers response, query
-
3
response.render view, playlists:, query:, layout: false
-
else: 9
else
-
9
response.render view, playlists:, query:
-
end
-
end
-
-
1
private
-
-
13
then: 8
else: 4
def load(query) = query.empty? ? repository.all : repository.search(:label, query)
-
-
1
def add_htmx_headers response, query
-
3
then: 1
else: 2
return if query.empty?
-
-
2
htmx.response! response.headers, push_url: routes.path(:playlists, query:)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Playlists
-
1
module Items
-
# The create action.
-
1
class Create < Action
-
1
include Deps[
-
repository: "repositories.playlist_item",
-
playlist_repository: "repositories.playlist",
-
show_view: "views.playlists.items.show"
-
]
-
-
1
params do
-
1
required(:playlist_id).filled :integer
-
2
required(:playlist_item).hash { required(:screen_id).filled :integer }
-
end
-
-
1
def handle request, response
-
3
parameters = request.params
-
3
playlist = playlist_repository.find parameters[:playlist_id]
-
-
3
else: 1
then: 2
halt :unprocessable_content unless parameters.valid? && playlist
-
-
1
response.render show_view, item: create(playlist, parameters), layout: false
-
end
-
-
1
private
-
-
1
def create playlist, parameters
-
1
item = repository.create_with_position playlist_id: playlist.id,
-
**parameters[:playlist_item]
-
-
1
playlist_repository.update_current_item playlist, item
-
1
item
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Playlists
-
1
module Items
-
# The index action.
-
1
class Index < Action
-
1
include Deps[repository: "repositories.playlist_item"]
-
-
1
def handle request, response
-
2
parameters = request.params
-
2
playlist_id = parameters[:playlist_id]
-
-
2
response.render view, playlist_id:, items: repository.where(playlist_id:), layout: false
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Playlists
-
1
module Mirror
-
# The edit action.
-
1
class Edit < Action
-
1
include Deps[
-
:htmx_layout,
-
repository: "repositories.playlist",
-
device_repository: "repositories.device"
-
]
-
-
2
params { required(:playlist_id).filled :integer }
-
-
1
def handle request, response
-
5
parameters = request.params
-
-
5
else: 4
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
4
response.render view,
-
playlist: repository.find(parameters[:playlist_id]),
-
devices: device_repository.all,
-
layout: htmx_layout.call(request)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Playlists
-
1
module Mirror
-
# The update action.
-
1
class Update < Action
-
1
include Deps[
-
:htmx,
-
:htmx_layout,
-
repository: "repositories.playlist",
-
device_repository: "repositories.device",
-
playlist_item_repository: "repositories.playlist_item",
-
view: "views.playlists.show"
-
]
-
-
1
params do
-
1
required(:playlist_id).filled :integer
-
2
optional(:mirror).filled(:hash) { required(:device_ids).array :integer }
-
end
-
-
1
def handle request, response
-
5
parameters = request.params
-
5
playlist = repository.find parameters[:playlist_id]
-
-
5
else: 4
then: 1
halt :not_found unless playlist
-
-
4
mirror playlist, parameters
-
4
render playlist, request, response
-
end
-
-
1
private
-
-
1
def mirror playlist, parameters
-
4
device_repository.mirror_playlist parameters.dig(:mirror, :device_ids), playlist.id
-
end
-
-
1
def render playlist, request, response
-
4
id = playlist.id
-
-
4
htmx.response! response.headers, push_url: routes.path(:playlist, id:)
-
4
response.render view,
-
playlist:,
-
items: playlist_item_repository.where(playlist_id: id),
-
layout: htmx_layout.call(request)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Playlists
-
# The new action.
-
1
class New < Action
-
1
include Deps[:htmx_layout]
-
-
1
def handle request, response
-
3
response.render view, fields: {mode: :automatic}, layout: htmx_layout.call(request)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Playlists
-
1
module Screens
-
# The index action.
-
1
class Index < Action
-
1
include Deps[repository: "repositories.playlist", view: "views.playlists.screens.show"]
-
1
include Initable[slide_window: Aspects::Playlists::SlideWindow]
-
-
2
params { required(:playlist_id).filled :integer }
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
else: 2
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
2
window = load_window parameters
-
2
before, current, after = window.screens
-
-
2
response.render view, playlist: window.playlist, before:, current:, after:
-
end
-
-
1
private
-
-
1
def load_window parameters
-
2
slide_window.new repository.with_screens.by_pk(parameters[:playlist_id]).one
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Playlists
-
1
module Screens
-
# The show action.
-
1
class Show < Action
-
1
include Deps[
-
:htmx_layout,
-
repository: "repositories.playlist",
-
item_repository: "repositories.playlist_item"
-
]
-
-
1
include Initable[slide_window: Aspects::Playlists::SlideWindow]
-
-
1
params do
-
1
required(:playlist_id).filled :integer
-
1
required(:id).filled :integer
-
end
-
-
1
def handle request, response
-
10
parameters = request.params
-
-
10
else: 9
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
9
response.render view, **view_settings(request, update_current_item(parameters))
-
end
-
-
1
private
-
-
1
def update_current_item parameters
-
9
playlist_id = parameters[:playlist_id]
-
-
9
repository.with_screens.by_pk(playlist_id).one.tap do |playlist|
-
9
then: 6
else: 3
return playlist if playlist.automatic?
-
-
3
item = item_repository.find_by playlist_id:, screen_id: parameters[:id]
-
3
repository.update playlist_id, current_item_id: item.id
-
end
-
end
-
-
1
def view_settings request, playlist
-
9
before, current, after = slide_window.new(playlist).screens request.params[:id]
-
-
{
-
9
playlist:,
-
before:,
-
current:,
-
after:,
-
layout: htmx_layout.call(request)
-
}
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Playlists
-
# The show action.
-
1
class Show < Action
-
1
include Deps[
-
:htmx_layout,
-
repository: "repositories.playlist",
-
item_repository: "repositories.playlist_item"
-
]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
else: 2
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
2
response.render view, **view_settings(request, parameters)
-
end
-
-
1
private
-
-
1
def view_settings request, parameters
-
2
playlist = repository.find parameters[:id]
-
-
{
-
2
playlist:,
-
items: item_repository.where(playlist_id: playlist.id),
-
layout: htmx_layout.call(request)
-
}
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Playlists
-
# The update action.
-
1
class Update < Action
-
1
include Deps[
-
repository: "repositories.playlist",
-
item_repository: "repositories.playlist_item",
-
show_view: "views.playlists.show"
-
]
-
-
1
params do
-
1
required(:id).filled :integer
-
-
1
required(:playlist).hash do
-
1
required(:label).filled :string
-
1
required(:name).filled :string
-
1
required(:mode).filled :string
-
end
-
end
-
-
1
def handle request, response
-
3
parameters = request.params
-
3
playlist = repository.find parameters[:id]
-
-
3
else: 2
then: 1
halt :unprocessable_content unless playlist
-
-
2
then: 1
if parameters.valid?
-
1
save playlist, parameters, response
-
else: 1
else
-
1
error playlist, parameters, response
-
end
-
end
-
-
1
private
-
-
1
def save playlist, parameters, response
-
1
id = playlist.id
-
1
repository.update id, **parameters[:playlist]
-
-
1
response.render show_view,
-
playlist: repository.find(id),
-
items: item_repository.where(playlist_id: id),
-
layout: false
-
end
-
-
1
def error playlist, parameters, response
-
1
response.render view,
-
playlist:,
-
fields: parameters[:playlist],
-
errors: parameters.errors[:playlist],
-
layout: false
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module ProblemDetails
-
# The index action.
-
1
class Index < Action
-
1
def handle(_request, response) = response.render view
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Screens
-
# The create action.
-
1
class Create < Action
-
1
include Deps[
-
:htmx,
-
repository: "repositories.screen",
-
model_repository: "repositories.model",
-
index_view: "views.screens.index"
-
]
-
-
1
params do
-
1
required(:screen).filled(:hash) do
-
1
required(:model_id).filled :integer
-
1
required(:label).filled :string
-
1
required(:name).filled :string
-
1
required(:image).filled :hash
-
end
-
end
-
-
1
def handle request, response
-
2
parameters = request.params
-
-
2
then: 1
if parameters.valid?
-
1
save parameters[:screen]
-
1
response.render index_view, screens: repository.all
-
else: 1
else
-
1
error response, parameters
-
end
-
end
-
-
1
private
-
-
# :reek:FeatureEnvy
-
# :reek:TooManyStatements
-
1
def save attributes
-
1
image = attributes.delete :image
-
1
record = repository.create attributes
-
1
tempfile = image[:tempfile]
-
1
extension = File.extname tempfile
-
-
1
record.upload tempfile, metadata: {"filename" => "#{record.name}#{extension}"}
-
1
repository.update record.id, image_data: record.image_attributes
-
end
-
-
1
def error response, parameters
-
1
response.render view,
-
models: model_repository.all,
-
screen: nil,
-
fields: parameters[:screen],
-
errors: parameters.errors[:screen]
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Screens
-
# The delete action.
-
1
class Delete < Action
-
1
include Deps[repository: "repositories.screen"]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
else: 2
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
2
repository.delete parameters[:id]
-
2
response.body = ""
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Screens
-
# The edit action.
-
1
class Edit < Action
-
1
include Deps[
-
:htmx_layout,
-
repository: "repositories.screen",
-
model_repository: "repositories.model"
-
]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
5
parameters = request.params
-
-
5
else: 4
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
4
response.render view,
-
models: model_repository.all,
-
screen: repository.find(parameters[:id]),
-
layout: htmx_layout.call(request)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Screens
-
# The index action.
-
1
class Index < Action
-
1
include Deps[:htmx, repository: "repositories.screen"]
-
-
1
def handle request, response
-
8
query = request.params[:query].to_s
-
8
screens = load query
-
-
8
then: 3
if htmx.request? request.env, :trigger, "search"
-
3
add_htmx_headers response, query
-
3
response.render view, screens:, query:, layout: false
-
else: 5
else
-
5
response.render view, screens:, query:
-
end
-
end
-
-
1
private
-
-
9
then: 4
else: 4
def load(query) = query.empty? ? repository.all : repository.search(:label, query)
-
-
1
def add_htmx_headers response, query
-
3
then: 1
else: 2
return if query.empty?
-
-
2
htmx.response! response.headers, push_url: routes.path(:screens, query:)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Screens
-
# The new action.
-
1
class New < Action
-
1
include Deps[:htmx_layout, model_repository: "repositories.model"]
-
-
1
def handle request, response
-
3
response.render view, models: model_repository.all, layout: htmx_layout.call(request)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Screens
-
# The show action.
-
1
class Show < Action
-
1
include Deps[:htmx_layout, repository: "repositories.screen"]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
5
parameters = request.params
-
-
5
else: 4
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
4
response.render view,
-
screen: repository.find(parameters[:id]),
-
layout: htmx_layout.call(request)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Screens
-
# The update action.
-
1
class Update < Action
-
1
include Deps[repository: "repositories.screen", model_repository: "repositories.model"]
-
-
1
params do
-
1
required(:id).filled :integer
-
-
1
required(:screen).filled(:hash) do
-
1
required(:model_id).filled :integer
-
1
required(:label).filled :string
-
1
required(:name).filled :string
-
1
optional(:image).filled :hash
-
end
-
end
-
-
1
def handle request, response
-
4
parameters = request.params
-
4
screen = repository.find parameters[:id]
-
-
4
else: 3
then: 1
halt :unprocessable_content unless screen
-
-
3
then: 2
if parameters.valid?
-
2
save screen, parameters, response
-
else: 1
else
-
1
error screen, parameters, response
-
end
-
end
-
-
1
private
-
-
# :reek:TooManyStatements
-
1
def save record, parameters, response
-
2
id = record.id
-
2
attributes = parameters[:screen]
-
2
image = attributes.delete :image
-
-
2
repository.update id, **attributes
-
2
attach record, image
-
2
response.redirect_to routes.path(:screen, id:)
-
end
-
-
# :reek:FeatureEnvy
-
1
def attach record, image
-
2
else: 1
then: 1
return unless image
-
-
1
tempfile = image[:tempfile]
-
1
extension = File.extname tempfile
-
-
1
record.replace tempfile, metadata: {"filename" => "#{record.name}#{extension}"}
-
1
repository.update record.id, image_data: record.image_attributes
-
end
-
-
1
def error screen, parameters, response
-
1
response.render view,
-
models: model_repository.all,
-
screen:,
-
fields: parameters[:screen],
-
errors: parameters.errors[:screen]
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Users
-
# The create action.
-
1
class Create < Action
-
1
include Deps[
-
:htmx_layout,
-
repository: "repositories.user",
-
status_repository: "repositories.user_status",
-
creator: "aspects.users.creator",
-
index_view: "views.users.index"
-
]
-
-
1
def handle request, response
-
4
case creator.call(**request.params.to_h.slice(:user))
-
in: 3
in Success(Structs::User)
-
3
in: 1
response.render index_view, users: repository.all, layout: htmx_layout.call(request)
-
1
in Failure(result) then error request, response, result
-
skipped
# :nocov:
-
skipped
# :nocov:
-
end
-
end
-
-
1
private
-
-
1
def error request, response, result
-
1
response.render view,
-
user: repository.find(request.params[:id]),
-
statuses: status_repository.all,
-
fields: result[:user],
-
errors: result.errors[:user],
-
layout: false
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Users
-
# The edit action.
-
1
class Edit < Action
-
1
include Deps[
-
:htmx_layout,
-
repository: "repositories.user",
-
status_repository: "repositories.user_status"
-
]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
4
parameters = request.params
-
-
4
else: 3
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
3
response.render view,
-
user: repository.find(parameters[:id]),
-
statuses: status_repository.all,
-
layout: htmx_layout.call(request)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Users
-
# The index action.
-
1
class Index < Action
-
1
include Deps[:htmx, repository: "repositories.user"]
-
-
1
def handle request, response
-
7
query = request.params[:query].to_s
-
7
users = load query
-
-
7
then: 3
if htmx.request? request.env, :trigger, "search"
-
3
add_htmx_headers response, query
-
3
response.render view, users:, query:, layout: false
-
else: 4
else
-
4
response.render view, users:, query:
-
end
-
end
-
-
1
private
-
-
8
then: 3
else: 4
def load(query) = query.empty? ? repository.all : repository.search(:name, query)
-
-
1
def add_htmx_headers response, query
-
3
then: 1
else: 2
return if query.empty?
-
-
2
htmx.response! response.headers, push_url: routes.path(:users, query:)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Users
-
# The new action.
-
1
class New < Action
-
1
include Deps[:htmx_layout, status_repository: "repositories.user_status"]
-
-
1
def handle request, response
-
3
response.render view, statuses: status_repository.all, layout: htmx_layout.call(request)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Users
-
# The show action.
-
1
class Show < Action
-
1
include Deps[:htmx_layout, repository: "repositories.user"]
-
-
2
params { required(:id).filled :integer }
-
-
1
def handle request, response
-
3
parameters = request.params
-
-
3
else: 2
then: 1
halt :unprocessable_content unless parameters.valid?
-
-
2
response.render view,
-
user: repository.find(parameters[:id]),
-
layout: htmx_layout.call(request)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Actions
-
1
module Users
-
# The update action.
-
1
class Update < Action
-
1
include Deps[
-
updater: "aspects.users.updater",
-
repository: "repositories.user",
-
status_repository: "repositories.user_status",
-
show_view: "views.users.show"
-
]
-
-
1
def handle request, response
-
2
in: 1
case updater.call(**request.params.to_h)
-
1
in: 1
in Success(user) then save user, response
-
1
in Failure(result) then error request, result, response
-
skipped
# :nocov:
-
skipped
# :nocov:
-
end
-
end
-
-
1
private
-
-
1
def save(user, response) = response.render show_view, user:, layout: false
-
-
1
def error request, result, response
-
1
response.render view,
-
user: repository.find(request.params[:id]),
-
statuses: status_repository.all,
-
fields: result[:user],
-
errors: result.errors[:user],
-
layout: false
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "functionable"
-
-
1
module Terminus
-
1
module Aspects
-
# Parses values into cron format.
-
1
module Croner
-
1
extend Functionable
-
-
1
def call interval = nil, unit = "minute", time: Time.utc(2025, 1, 1, 0, 0, 0)
-
34
when: 13
case unit
-
13
when: 3
when "minute" then for_minute interval, time
-
3
when: 3
when "hour" then for_hour interval, time
-
3
when: 5
when "day" then for_day interval, time
-
5
when: 7
when "week" then for_week interval, time
-
7
when: 2
when "month" then for_month interval, time
-
2
else: 1
when "none" then Core::EMPTY_STRING
-
1
else fail ArgumentError, "Unknown unit: #{unit.inspect}."
-
end
-
end
-
-
1
def for_minute interval, time
-
13
zone = time.zone
-
13
then: 5
else: 8
interval ? "*/#{interval} * * * * #{zone}" : "* * * * * #{zone}"
-
end
-
-
1
def for_hour interval, time
-
3
_, minute, *, zone = time.to_a
-
-
3
in: 2
case [interval, time]
-
2
else: 1
in Integer, Time then "#{minute} */#{interval} * * * #{zone}"
-
1
else "#{minute} * * * * #{zone}"
-
end
-
end
-
-
1
def for_day interval, time
-
3
_, minute, hour, *, zone = time.to_a
-
-
3
in: 2
case [interval, time]
-
2
else: 1
in Integer, Time then "#{minute} #{hour} */#{interval} * * #{zone}"
-
1
else "#{minute} #{hour} * * * #{zone}"
-
end
-
end
-
-
1
def for_week interval, time
-
5
_, minute, hour, *, zone = time.to_a
-
-
5
in: 1
case interval
-
1
in: 3
in Integer then "#{minute} #{hour} * * #{interval} #{zone}"
-
3
else: 1
in Array then %(#{minute} #{hour} * * #{interval.join ","} #{zone})
-
1
else "#{minute} #{hour} * * 0 #{zone}"
-
end
-
end
-
-
1
def for_month interval, time
-
7
_, minute, hour, *, zone = time.to_a
-
-
7
in: 2
case [interval, time]
-
2
in Integer, Time then "#{minute} #{hour} * */#{interval} * #{zone}"
-
in: 2
in String, Time
-
2
part, directive = interval.scan(/\d+|\D+/)
-
2
in: 2
%(#{minute} #{hour} #{directive} */#{part} * #{zone})
-
2
else: 1
in Array, Time then %(#{minute} 0 #{interval.join ","} * * #{zone})
-
1
else "#{minute} #{hour} 1 * * #{zone}"
-
end
-
end
-
-
1
conceal %i[for_minute for_hour for_day for_week for_month]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "securerandom"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Devices
-
# Builds default attributes for new devices.
-
1
class Defaulter
-
1
def initialize randomizer: SecureRandom, mac_address_builder: Devices::MACAddressBuilder
-
40
@randomizer = randomizer
-
40
@mac_address_builder = mac_address_builder
-
end
-
-
1
def call
-
{
-
33
api_key: randomizer.alphanumeric(20),
-
mac_address: mac_address_builder.call,
-
firmware_update: true,
-
friendly_id: randomizer.hex(3).upcase,
-
image_timeout: 0,
-
label: "TRMNL",
-
refresh_rate: 900
-
}
-
end
-
-
1
private
-
-
1
attr_reader :randomizer, :mac_address_builder
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "securerandom"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Devices
-
# Builds a random, locally administered, unicast MAC address.
-
1
MACAddressBuilder = lambda do |randomizer: SecureRandom|
-
47
zero_mask = 0xFC # 11111100
-
47
local_mask = 0x02 # 00000010
-
47
bytes = randomizer.bytes(6).unpack "C*"
-
47
bytes[0] = (bytes[0] & zero_mask) | local_mask
-
-
329
bytes.map { format "%02X", it }
-
.join ":"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "pipeable"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Devices
-
# Handles the setup and default configuration of new devices.
-
1
class Provisioner
-
1
include Deps[
-
"aspects.devices.defaulter",
-
"aspects.screens.welcomer",
-
repository: "repositories.device",
-
playlist_repository: "repositories.playlist",
-
item_repository: "repositories.playlist_item"
-
]
-
-
1
include Dry::Monads[:result]
-
1
include Pipeable
-
-
1
def call(mac_address: MACAddressBuilder.call, **)
-
30
device = repository.find_by(mac_address:)
-
-
30
then: 2
else: 28
return Success device if device
-
-
28
process(mac_address, **)
-
end
-
-
1
private
-
-
1
def process(mac_address, **)
-
28
cached_device = nil
-
-
28
pipe(
-
create(mac_address, **),
-
24
fmap { cached_device = it },
-
24
bind { |device| welcomer.call device },
-
24
fmap { |screen| configure cached_device, screen }
-
)
-
end
-
-
1
def create(mac_address, **)
-
28
Success repository.create(defaulter.call.merge!(mac_address:, **))
-
rescue ROM::SQL::NotNullConstraintError => error
-
2
Failure "#{error.message.match(/ERROR: (.+)\n/)[1].capitalize}."
-
rescue ROM::SQL::ForeignKeyConstraintError => error
-
2
Failure error.message.sub(/.+DETAIL: /m, "").strip
-
end
-
-
1
def configure device, screen
-
24
playlist_id = create_playlist_id device
-
24
item = item_repository.create_with_position playlist_id:, screen_id: screen.id
-
-
24
playlist_repository.update playlist_id, current_item_id: item.id
-
24
repository.update device.id, playlist_id:
-
end
-
-
1
def create_playlist_id device
-
24
id = device.friendly_id
-
24
playlist = playlist_repository.create label: "Device #{id}", name: "device_#{id.downcase}"
-
-
24
playlist.id
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "dry/monads"
-
1
require "initable"
-
1
require "refinements/array"
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Devices
-
1
module Sensors
-
# Creates or updates device sensor records based on hardware readings.
-
1
class Synchronizer
-
1
include Deps[
-
:settings,
-
:logger,
-
device_relation: "relations.device",
-
sensor_repository: "repositories.device_sensor"
-
]
-
1
include Dry::Monads[:result]
-
1
include Initable[schema: proc { Terminus::Schemas::Devices::Sensors::Upsert }]
-
-
1
using Refinements::Array
-
1
using Refinements::Hash
-
-
9
def call = load.then { |data| process_devices data }
-
-
1
private
-
-
1
def load
-
8
path = settings.sensors_path
-
-
8
then: 5
else: 3
return JSON path.read if path.exist?
-
-
6
logger.debug { "Sensors path not found: #{path}. Skipped." }
-
3
Core::EMPTY_HASH
-
end
-
-
1
def process_devices data
-
8
device_relation.select(:id)
-
6
.map { it[:id] }
-
6
.each { |id| process_sensors id, data }
-
end
-
-
1
def process_sensors device_id, data
-
6
data.fetch("data", Core::EMPTY_ARRAY).map do |entry|
-
7
result = schema.call entry
-
-
7
then: 5
if result.success?
-
5
deduplicate device_id, result.to_h
-
else: 2
else
-
2
log_error result
-
end
-
end
-
end
-
-
1
def deduplicate device_id, attributes
-
5
then: 2
if find_with device_id, attributes
-
4
logger.debug(tags: [attributes]) { "Duplicate sensor detected. Skipped." }
-
else: 3
else
-
3
sensor_repository.create device_id:, source: "server", **attributes
-
end
-
end
-
-
1
def find_with device_id, attributes
-
10
attributes.transform_value!(:created_at) { Time.at(it).utc }
-
-
5
make, model, kind, created_at = attributes.values_at :make, :model, :kind, :created_at
-
-
5
sensor_repository.find_by device_id:,
-
make:,
-
model:,
-
kind:,
-
created_at:,
-
source: "device"
-
end
-
-
1
def log_error result
-
2
message = result.errors
-
.to_h
-
4
.map { |key, value| "#{key} #{value.to_sentence}" }
-
.to_sentence delimiter: "; "
-
-
4
logger.error { "Unable to validate sensor: #{message}." }
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "pipeable"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Devices
-
# Updates device based on firmware header information.
-
1
class Synchronizer
-
1
include Deps[
-
:settings,
-
firmware_parser: "aspects.firmware.headers.parser",
-
repository: "repositories.device"
-
]
-
1
include Pipeable
-
1
include Dry::Monads[:result]
-
-
1
def call(headers) = pipe firmware_parser.call(headers), :update
-
-
1
private
-
-
1
def update result, at: Time.now
-
11
result.bind do |payload|
-
10
device = repository.update_by_mac_address payload.mac_address,
-
**payload.device_attributes,
-
synced_at: at
-
10
then: 7
else: 3
device ? Success(device) : Failure("Unable to find device by MAC address.")
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Terminus
-
1
module Aspects
-
# A simple content downloader.
-
1
class Downloader
-
1
include Deps[:http, :logger]
-
1
include Dry::Monads[:result]
-
-
16
def call(uri) = get(uri).tap { log it, uri }
-
-
1
private
-
-
1
def get uri
-
15
http.get(uri).then do |response|
-
8
then: 6
else: 2
response.status.success? ? Success(response) : Failure(response)
-
end
-
rescue HTTP::RequestError, OpenSSL::SSL::SSLError => error
-
6
Failure error.message
-
end
-
-
1
def log result, uri
-
15
in: 6
case result
-
12
in: 2
in Success then logger.info { "Downloaded: #{uri}." }
-
2
in: 6
in Failure(HTTP::Response => response) then log_error response.body
-
6
else: 1
in Failure(String => message) then log_error message
-
1
else log_error "Unable to download: #{uri.inspect}."
-
end
-
-
15
result
-
end
-
-
10
def log_error(message) = logger.error { message }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
# Clones an existing extension.
-
1
class Cloner
-
1
include Deps[
-
"aspects.jobs.schedule",
-
repository: "repositories.extension",
-
exchange_repository: "repositories.extension_exchange"
-
]
-
-
1
include Dry::Monads[:result]
-
-
1
def call(id, **overrides)
-
10
Success create(id, build_attributes(id, overrides))
-
rescue ROM::SQL::UniqueConstraintError => error
-
3
build_failure error.message
-
end
-
-
1
private
-
-
1
def build_attributes id, overrides
-
10
original = repository.find id
-
-
10
{
-
**original.to_h.except(:id, :created_at, :updated_at),
-
label: "#{original.label} Clone",
-
name: "#{original.name}_clone",
-
**overrides
-
}
-
end
-
-
1
def create id, attributes
-
10
extension = create_with_models attributes
-
-
7
add_devices extension, attributes
-
7
add_exchanges id, extension
-
7
add_schedule extension
-
7
extension
-
end
-
-
1
def create_with_models attributes
-
10
repository.create_with_models attributes, Array(attributes[:model_ids])
-
end
-
-
1
def add_devices extension, attributes
-
7
repository.update_with_devices extension.id, {}, Array(attributes[:device_ids])
-
end
-
-
1
def add_exchanges original_id, extension
-
7
exchange_repository.where(extension_id: original_id).each do |original|
-
1
exchange_repository.create extension_id: extension.id,
-
**original.to_h.except(
-
:id,
-
:extension_id,
-
:created_at,
-
:updated_at
-
)
-
end
-
end
-
-
1
def add_schedule extension
-
7
schedule.upsert(*extension.to_schedule)
-
end
-
-
1
def build_failure message
-
3
match = message.match(/Key \((?<key>[^)]+)\)/)
-
3
Failure match[:key].to_sym => ["must be unique"]
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
# Assembles the Liquid context for rendering screens.
-
1
class Contextualizer
-
1
include Deps["aspects.models.finder", sensor_repository: "repositories.device_sensor"]
-
-
1
using Refinements::Hash
-
-
1
def call extension, model_id: nil, device_id: nil
-
{
-
40
"extension" => extension.liquid_attributes.merge!(
-
"css_classes" => build_screen_classes(model_id, device_id)
-
),
-
"sensors" => load_sensors(device_id)
-
}
-
end
-
-
1
private
-
-
1
def build_screen_classes model_id, device_id
-
40
model = finder.call(model_id:, device_id:).value_or(nil)
-
40
then: 16
else: 24
model.css_classes if model
-
end
-
-
1
def load_sensors(device_id) = sensor_repository.where(device_id:).map(&:liquid_attributes)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "initable"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
# Renders curl command for exchange and associated data.
-
1
class Curler
-
1
include Deps["aspects.extensions.uri_builder"]
-
1
include Initable[json_formatter: proc { Terminus::Aspects::JSONFormatter }]
-
-
1
def self.render_request verb, uri
-
9
then: 8
else: 1
verb.include?("get") ? "curl #{uri}" : "curl --request #{verb.upcase} #{uri}"
-
end
-
-
1
def self.render_headers attributes
-
9
then: 5
else: 4
return if Hash(attributes).empty?
-
-
10
attributes.map { |key, value| "--header '#{key.downcase}: #{value}'" }
-
end
-
-
1
def call extension, exchange
-
9
uri_builder.call(extension, exchange.template)
-
9
.map { |uri| render uri, exchange }
-
.join "\n"
-
end
-
-
1
private
-
-
1
def render uri, exchange
-
9
klass = self.class
-
-
[
-
9
klass.render_request(exchange.verb, uri),
-
*klass.render_headers(exchange.headers),
-
render_body(exchange.body)
-
].compact
-
.each
-
.with_index
-
19
then: 9
else: 10
.map { |line, index| index.zero? ? line : " #{line}" }
-
.join " \\\n"
-
end
-
-
1
def render_body attributes
-
9
then: 5
else: 4
return if Hash(attributes).empty?
-
-
4
"--data $'#{json_formatter.call attributes}'"
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
DEFAULTS = {
-
1
tags: [],
-
mode: "light",
-
kind: "poll",
-
verb: "get",
-
start_at: Time.now.strftime("%Y-%m-%dT00:00:00"),
-
days: [],
-
interval: 1,
-
template: <<~BODY
-
<div class="{{extension.css_classes}}">
-
<div class="view view--full">
-
<div class="layout layout--col">
-
</div>
-
</div>
-
</div>
-
BODY
-
}.freeze
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Exchanges
-
# Answers coalesced (and sequenced) data for all exchanges.
-
1
Coalescer = lambda do |exchanges, index: 1|
-
26
exchanges.each.with_object({}) do |exchange, all|
-
9
exchange.data.each do |key, value|
-
13
all[key.sub(/\d+/, index.to_s)] = value
-
13
index += 1
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/core"
-
1
require "dry/monads"
-
1
require "initable"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Exchanges
-
# Updates an exchange based on multiple responses.
-
1
class Refresher
-
1
include Deps[
-
"aspects.extensions.uri_builder",
-
fetcher: "aspects.extensions.fetchers.sole",
-
extension_repository: "repositories.extension",
-
exchange_repository: "repositories.extension_exchange"
-
]
-
1
include Initable[input: Fetchers::Input]
-
1
include Dry::Monads[:result]
-
-
1
def call exchange
-
7
extension_id = exchange.extension_id
-
7
extension = extension_repository.find extension_id
-
-
7
else: 6
then: 1
return Failure "Unable to find extension by ID: #{extension_id}." unless extension
-
-
6
update exchange, build_inputs(exchange, extension)
-
end
-
-
1
private
-
-
1
def build_inputs exchange, extension
-
6
uri_builder.call(extension, exchange.template).map do |uri|
-
7
input[uri:, **exchange.http_attributes]
-
end
-
end
-
-
1
def update exchange, inputs
-
6
id = exchange.id
-
6
payloads = fetch inputs
-
-
6
exchange_repository.update id, **payloads, refreshed_at: Time.now
-
6
Success exchange_repository.find id
-
end
-
-
# :reek:FeatureEnvy
-
# :reek:TooManyStatements
-
1
def fetch inputs, data: {}, errors: {}
-
6
inputs.each.with_index 1 do |input, index|
-
7
key = "source_#{index}"
-
-
7
in: 3
case fetcher.call input
-
3
in: 3
in Success(payload) then data.merge! key => payload[:data]
-
3
else: 1
in Failure(payload) then errors.merge! key => payload[:error]
-
1
else errors.merge! key => "Unable to fetch, invalid result."
-
end
-
end
-
-
6
{data:, errors:}
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
# Exports extension attributes for sharing.
-
1
class Exporter
-
1
include Deps[:settings, exchange_repository: "repositories.extension_exchange"]
-
1
include Dry::Monads[:result]
-
-
1
def call extension
-
3
exchange_repository.where(extension_id: extension.id)
-
.map(&:export_attributes)
-
.then do |exchanges|
-
3
Success(
-
version: settings.git_tag,
-
**extension.export_attributes,
-
exchanges:
-
)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Fetchers
-
# The input for HTTP requests.
-
1
Input = Data.define :headers, :verb, :uri, :body do
-
1
def initialize uri:, headers: Core::EMPTY_HASH, verb: "get", body: Core::EMPTY_HASH
-
39
super
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "dry/monads"
-
1
require "initable"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Fetchers
-
# Processes a single request.
-
1
class Sole
-
1
include Deps[:http]
-
1
include Initable[parser: Extensions::Parser, special_header: "Accept"]
-
1
include Dry::Monads[:result]
-
-
1
def call input
-
32
process(input).fmap { maybe_alter_mime_type input.headers, it }
-
13
.fmap { |mime_type, body| parse mime_type, body }
-
13
.bind { build_success input, it }
-
end
-
-
1
private
-
-
1
def process input
-
19
http.headers(input.headers)
-
.follow
-
.public_send(input.verb, input.uri)
-
15
then: 13
else: 2
.then { it.status.success? ? Success(it) : build_detailed_failure(input, it) }
-
1
rescue HTTP::RequestError then build_failure input, "Unable to make request"
-
1
rescue HTTP::ConnectionError then build_failure input, "Unable to connect"
-
1
rescue HTTP::TimeoutError then build_failure input, "Connection timed out"
-
1
rescue OpenSSL::SSL::SSLError then build_failure input, "Unable to secure connection"
-
end
-
-
1
def maybe_alter_mime_type headers, response
-
13
type = headers && headers[special_header]
-
13
[type || response.mime_type, response.body]
-
end
-
-
1
def parse type, body
-
13
when: 4
case type
-
4
when: 1
when %r(application/([[:alnum:]][\w!#&-^$]*\+)?json) then parser.from_json body
-
1
when: 1
when %r(image/.+) then parser.from_image body
-
1
when: 1
when "text/csv" then parser.from_csv body
-
1
when "text/plain" then parser.from_text body
-
when: 4
when "text/xml", "application/xml", "application/rss+xml", "application/atom+xml"
-
4
else: 2
parser.from_xml body
-
2
else Failure "Unknown MIME Type: #{type}."
-
end
-
end
-
-
# :reek:FeatureEnvy
-
1
def build_success input, result
-
13
then: 11
if result.success?
-
11
Success data: result.success, error: Core::EMPTY_HASH
-
else: 2
else
-
2
build_failure input, result.failure
-
end
-
end
-
-
1
def build_failure input, body
-
6
Failure data: Core::EMPTY_HASH, error: {uri: input.uri, code: nil, type: nil, body:}
-
end
-
-
# :reek:FeatureEnvy
-
1
def build_detailed_failure input, error
-
2
Failure data: Core::EMPTY_HASH,
-
error: {
-
uri: input.uri,
-
code: error.code,
-
type: error.mime_type,
-
body: error.body
-
}
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "dry/monads"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Importers
-
1
module Remote
-
# Creates extension from plugin (recipe).
-
1
class Creator
-
1
include Deps[
-
:logger,
-
"aspects.extensions.importers.remote.transformer",
-
keyer: "aspects.extensions.importers.remote.transformers.template_keys",
-
repository: "repositories.extension",
-
model_repository: "repositories.model",
-
exchange_repository: "repositories.extension_exchange"
-
]
-
-
1
include Dry::Monads[:result]
-
-
1
def initialize(problem_detail: Aspects::ProblemDetail, **)
-
7
super(**)
-
7
@problem_detail = problem_detail
-
end
-
-
1
def call id
-
7
transform id
-
rescue ROM::SQL::UniqueConstraintError => error
-
1
Failure problem_detail.duplicate(error.message, nil).detail
-
end
-
-
1
private
-
-
1
attr_reader :problem_detail
-
-
1
def transform id
-
7
transformer.call(id).fmap do |attributes|
-
7
record = repository.create_with_models attributes, model_ids
-
6
id = record.id
-
-
6
add_exchanges id, attributes
-
6
repository.find id
-
end
-
end
-
-
1
def model_ids
-
7
model_repository.find_by(name: "og_plus").then do |model|
-
7
then: 1
else: 6
model ? [model.id] : Core::EMPTY_ARRAY
-
end
-
end
-
-
1
def add_exchanges extension_id, attributes
-
6
headers, verb, templates, body = attributes.values_at :poll_headers,
-
:poll_verb,
-
:poll_template,
-
:poll_body
-
-
6
templates.each do |content|
-
6
template = transform_exchange_template content
-
6
exchange_repository.create extension_id:, headers:, verb:, template:, body:
-
end
-
end
-
-
1
def transform_exchange_template content
-
6
in: 3
case keyer.call content
-
3
in Success(content) then content
-
in: 2
in Failure(message)
-
4
logger.debug { message }
-
2
Core::EMPTY_STRING
-
else: 1
else
-
2
logger.error { "Unable to transform exchange template." }
-
1
Core::EMPTY_STRING
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "initable"
-
1
require "refinements/pathname"
-
1
require "zip"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Importers
-
1
module Remote
-
# Downloads and decompresses a TRMNL plugin archive.
-
1
class Extractor
-
1
include Deps["aspects.downloader"]
-
-
1
include Initable[
-
uri: "https://usetrmnl.com/api/plugin_settings/%<id>s/archive",
-
client: Zip::File
-
]
-
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::Pathname
-
-
1
def call id
-
4
format(uri, id:).then { downloader.call it }
-
2
.fmap { |response| extract response }
-
rescue Zip::Error => error
-
1
Failure error.message
-
end
-
-
1
private
-
-
1
def extract response, content: {}
-
3
client.open_buffer(response.body.to_s) { |zip| build content, zip }
-
1
content
-
end
-
-
1
def build content, zip
-
1
zip.each do |entry|
-
6
key = Pathname(entry.name).name.to_s.to_sym
-
6
content[key] = entry.get_input_stream.read
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Importers
-
1
module Remote
-
# Defines import schema.
-
1
Schema = Dry::Schema.Params do
-
1
optional(:custom_fields).array(:hash)
-
1
required(:dark_mode).filled :bool
-
1
required(:name).filled :string
-
1
required(:polling_body).maybe :hash
-
1
required(:polling_headers).maybe :hash
-
1
required(:polling_url).maybe :string
-
1
required(:polling_verb).filled :string
-
1
required(:refresh_interval).filled :integer
-
1
required(:static_data).maybe :hash
-
1
required(:strategy).filled :string
-
-
1
after(:value_coercer, &Schemas::Coercers::JSONToHash.curry[:polling_body])
-
1
after(:value_coercer, &Schemas::Coercers::URIQueryToHash.curry[:polling_headers])
-
1
after(:value_coercer, &Schemas::Coercers::JSONToHash.curry[:static_data])
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Importers
-
1
module Remote
-
# Transforms remote plugin (recipe) data into extension attributes.
-
1
class Transformer
-
1
include Deps[
-
"aspects.extensions.importers.remote.extractor",
-
"aspects.extensions.importers.remote.transformers.data",
-
"aspects.extensions.importers.remote.transformers.default",
-
"aspects.extensions.importers.remote.transformers.keys",
-
"aspects.extensions.importers.remote.transformers.kind",
-
"aspects.extensions.importers.remote.transformers.template",
-
"aspects.extensions.importers.remote.transformers.poll"
-
]
-
-
1
include Dry::Monads[:result]
-
-
1
def initialize(schema: Importers::Remote::Schema, **)
-
5
@schema = schema
-
5
super(**)
-
end
-
-
6
def call(id) = extractor.call(id).bind { |archive| process archive }
-
-
1
private
-
-
1
attr_reader :schema
-
-
1
def process archive
-
# Order matters.
-
9
validate(archive).bind { |attributes| keys.call attributes.to_h }
-
4
.bind { |attributes| poll.call attributes }
-
4
.bind { |attributes| kind.call attributes }
-
3
.bind { |attributes| data.call attributes }
-
3
.bind { |attributes| template.call attributes, archive }
-
3
.bind { |attributes| default.call attributes }
-
end
-
-
1
def validate archive
-
5
then: 1
if archive.key? :transform
-
1
Failure "Serverless transforms are not supported yet."
-
else: 4
else
-
4
YAML.load(archive[:settings])
-
4
.then { |settings| schema.call settings }
-
.to_monad
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "dry/monads"
-
1
require "initable"
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Importers
-
1
module Remote
-
1
module Transformers
-
# Transforms custom field defaults into data defaults.
-
1
class Data
-
1
include Initable[target: :fields, keys: %w[keyname default]]
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::Hash
-
-
1
def call attributes
-
6
values = attributes.fetch(target, Core::EMPTY_HASH)
-
.each
-
.with_object({}) do |item, all|
-
6
key, value = item.values_at(*keys)
-
6
then: 5
else: 1
all[key] = value if value
-
end
-
-
6
Success attributes.merge!(data: {"values" => values}.compress)
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "initable"
-
1
require "refinements/string"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Importers
-
1
module Remote
-
1
module Transformers
-
# Transforms (mutates) by adding defaults for initialization.
-
1
class Default
-
1
include Initable[description: "Imported from TRMNL.", unit: "minute"]
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::String
-
-
1
def call attributes
-
5
Success attributes.merge!(
-
name: attributes[:label].snakecase.tr("/", "_"),
-
description:,
-
interval: 1,
-
unit: "none"
-
)
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "initable"
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Importers
-
1
module Remote
-
1
module Transformers
-
# Transforms (mutates) attributes for initialization.
-
1
class Keys
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::Hash
-
-
1
include Initable[
-
map: {
-
name: :label,
-
polling_headers: :poll_headers,
-
polling_verb: :poll_verb,
-
polling_url: :poll_template,
-
polling_body: :poll_body,
-
custom_fields: :fields,
-
refresh_interval: :interval
-
},
-
deletes: %i[dark_mode]
-
]
-
-
1
def call attributes
-
10
deletes.each { attributes.delete it }
-
5
attributes.transform_keys! map
-
5
Success attributes
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "refinements/array"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Importers
-
1
module Remote
-
1
module Transformers
-
# Transforms (mutates) strategy and polling/static attributes for initialization.
-
1
class Kind
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::Array
-
-
1
KINDS = {"polling" => "poll", "static" => "static"}.freeze
-
-
1
def initialize kinds: KINDS
-
9
@kinds = kinds
-
9
@keys = kinds.keys
-
end
-
-
1
def call attributes
-
8
strategy = attributes.delete :strategy
-
-
8
then: 5
if keys.include? strategy
-
5
Success process(strategy, attributes)
-
else: 3
else
-
3
Failure "Unsupported kind: #{strategy}. Use: #{keys.to_sentence :or}."
-
end
-
end
-
-
1
private
-
-
1
attr_reader :kinds, :keys
-
-
1
def process strategy, attributes
-
5
kind = kinds[strategy]
-
5
static_data = attributes.delete :static_data
-
-
5
then: 2
if kind == "static"
-
2
attributes.merge! kind:, static_body: static_data
-
else: 3
else
-
3
attributes.merge! kind:, poll_body: attributes[:poll_body]
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "initable"
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Importers
-
1
module Remote
-
1
module Transformers
-
# Transforms poll URIs/templates.
-
1
class Poll
-
1
include Initable[
-
key: :poll_template,
-
line_pattern: /\r\n|\n|\r|\s/,
-
liquid_pattern: /\{\{.+\}\}/m
-
]
-
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::Hash
-
-
1
def call attributes
-
9
value = String attributes[key]
-
-
9
then: 2
if value.match? liquid_pattern
-
2
attributes[key] = [value]
-
2
Success attributes
-
else: 7
else
-
14
Success attributes.transform_value!(key) { value.split line_pattern }
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "initable"
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Importers
-
1
module Remote
-
1
module Transformers
-
# Transforms (mutates) the full Liquid template for initialization.
-
1
class Template
-
1
include Deps[keyer: "aspects.extensions.importers.remote.transformers.template_keys"]
-
1
include Initable[
-
layout: <<~BODY
-
<div class="{{extension.css_classes}}">
-
<div class="view view--full">
-
%<content>s
-
</div>
-
</div>
-
BODY
-
]
-
-
1
using Refinements::Hash
-
-
1
def call attributes, archive
-
12
merge_content(archive).then { keyer.call it }
-
6
.fmap { attributes.merge! template: it }
-
end
-
-
1
private
-
-
1
def merge_content archive
-
6
archive.use do |shared, full|
-
6
full_transform = format layout, content: full
-
6
[shared, full_transform].compact.join "\n\n"
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "initable"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Importers
-
1
module Remote
-
1
module Transformers
-
# Transforms a Liquid template to use Terminus keys instead of TRMNL keys.
-
1
class TemplateKeys
-
1
include Initable[
-
key_map: {
-
"rss." => "source_1.rss.",
-
"source_1.data" => "source_1",
-
"trmnl.plugin_settings.instance_name" => "extension.label",
-
"trmnl.plugin_settings.custom_fields_values" => "extension.values",
-
"trmnl.plugin_settings.custom_fields[0]" => "extension.fields[0]"
-
},
-
index_pattern: /
-
(?<prefix>IDX) # Prefix
-
_ # Delimiter
-
(?<index>\d+) # Index
-
/mx
-
]
-
-
1
include Dry::Monads[:result]
-
-
1
def call content
-
11
mutation = content.dup
-
-
11
format_sources mutation
-
11
format_fields mutation
-
-
11
Success mutation
-
end
-
-
1
private
-
-
1
def format_sources content, offset: 1
-
11
content.gsub! index_pattern do
-
9
captures = Regexp.last_match.named_captures
-
9
"source_#{captures["index"].to_i + offset}"
-
end
-
end
-
-
1
def format_fields content
-
66
key_map.each { |original, modification| content.gsub! original, modification }
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "csv"
-
1
require "dry/monads"
-
1
require "functionable"
-
1
require "json"
-
1
require "nori"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
# Parses supported data types into a hash for further processing.
-
1
module Parser
-
1
extend Dry::Monads[:result]
-
1
extend Functionable
-
-
1
def from_csv body
-
7
Success ::CSV.parse(String(body), headers: true).each.map(&:to_h)
-
rescue ::CSV::MalformedCSVError => error
-
2
Failure error.message
-
end
-
-
1
def from_image(body) = Success body
-
-
1
def from_json body
-
9
then: 2
else: 7
content = String(body).empty? ? Core::EMPTY_ARRAY : JSON(body)
-
8
Success content
-
rescue ::JSON::ParserError => error
-
1
Failure "#{error.message.capitalize}."
-
end
-
-
1
def from_text body
-
6
Success String(body).split
-
rescue ArgumentError => error
-
1
Failure "#{error.message.capitalize}."
-
end
-
-
1
def from_xml body, nori: Nori.new(parser: :rexml)
-
9
content = nori.parse String(body)
-
8
then: 2
else: 6
Success content.empty? ? Core::EMPTY_ARRAY : content
-
rescue REXML::ParseException => error
-
1
Failure error.message
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
# Renders extension based on kind.
-
1
class Renderer
-
1
include Deps[
-
"aspects.extensions.contextualizer",
-
"aspects.extensions.renderers.image",
-
"aspects.extensions.renderers.poll",
-
"aspects.extensions.renderers.static"
-
]
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::Hash
-
-
1
def call extension, model_id: nil, device_id: nil
-
16
process extension, contextualizer.call(extension, model_id:, device_id:)
-
end
-
-
1
private
-
-
1
def process extension, context
-
16
kind = extension.kind
-
-
16
when: 1
case kind
-
1
when: 11
when "image" then image.call extension, context:
-
11
when: 3
when "poll" then poll.call extension, context:
-
3
else: 1
when "static" then static.call extension, context:
-
1
else Failure "Unsupported extension kind: #{kind}."
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "dry/monads"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Renderers
-
# Uses Liquid template to render images.
-
1
class Image
-
1
include Deps[
-
exchange_repository: "repositories.extension_exchange",
-
renderer: "liquid.sanitize"
-
]
-
1
include Dry::Monads[:result]
-
-
1
def call extension, context: Core::EMPTY_HASH
-
2
exchanges = exchange_repository.where extension_id: extension.id
-
-
2
then: 1
if exchanges.one?
-
1
content = renderer.call(
-
extension.template,
-
{**context, "source_1" => {"url" => exchanges.first.template}}
-
)
-
-
1
Success content
-
else: 1
else
-
1
render_many extension, exchanges, context
-
end
-
end
-
-
1
private
-
-
1
def render_many extension, exchanges, context
-
1
data = exchanges.each.with_index(1).with_object({}) do |(exchange, index), all|
-
2
all["source_#{index}"] = {"url" => exchange.template}
-
end
-
-
1
Success renderer.call(extension.template, context.merge(data))
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "dry/monads"
-
1
require "initable"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Renderers
-
# Uses Liquid template to render poll data.
-
1
class Poll
-
1
include Deps[
-
"aspects.extensions.exchanges.refresher",
-
exchange_repository: "repositories.extension_exchange",
-
renderer: "liquid.sanitize"
-
]
-
1
include Dry::Monads[:result]
-
1
include Initable[coalescer: proc { Terminus::Aspects::Extensions::Exchanges::Coalescer }]
-
-
1
def call extension, context: Core::EMPTY_HASH
-
14
refresh extension.id
-
14
render extension, context
-
end
-
-
1
private
-
-
1
def refresh extension_id
-
17
exchange_repository.where(extension_id:).each { refresher.call it }
-
end
-
-
1
def render extension, context
-
14
exchanges = exchange_repository.where extension_id: extension.id
-
14
data = coalescer.call exchanges
-
-
14
Success renderer.call(extension.template, context.merge(data))
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "dry/monads"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
1
module Renderers
-
# Uses Liquid template to render static data.
-
1
class Static
-
1
include Deps[renderer: "liquid.sanitize"]
-
1
include Dry::Monads[:result]
-
-
1
def call extension, context: Core::EMPTY_HASH
-
3
Success renderer.call(
-
extension.template,
-
context.merge("source_1" => extension.static_body)
-
)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
# Creates or updates associated screen from Liquid content.
-
1
class ScreenUpserter
-
1
include Deps[
-
"aspects.extensions.renderer",
-
"aspects.screens.upserter",
-
view: "views.extensions.dynamic"
-
]
-
-
1
def call extension, model_id: nil, device_id: nil
-
8
renderer.call(extension, model_id:, device_id:)
-
8
.fmap { view.call content: it }
-
.bind do |content|
-
8
upserter.call model_id:,
-
device_id:,
-
content: String.new(content),
-
**extension.screen_attributes
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Extensions
-
# A specialized URI builder based template and data to produce an array of fully formed URIs.
-
1
class URIBuilder
-
1
include Deps["aspects.extensions.contextualizer", renderer: "liquid.basic"]
-
-
1
def call extension, template
-
22
contextualizer.call(extension)
-
22
.then { |data| renderer.call(template, data).split }
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Firmware
-
1
module Headers
-
1
KEY_MAP = {
-
HTTP_ACCESS_TOKEN: :api_key,
-
HTTP_BATTERY_VOLTAGE: :battery_voltage,
-
HTTP_FW_VERSION: :firmware_version,
-
HTTP_HEIGHT: :height,
-
HTTP_HOST: :host,
-
HTTP_ID: :mac_address,
-
HTTP_MODEL: :model_name,
-
HTTP_PERCENT_CHARGED: :battery_charge,
-
HTTP_REFRESH_RATE: :refresh_rate,
-
HTTP_RSSI: :wifi,
-
HTTP_SENSORS: :sensors,
-
HTTP_UPDATE_SOURCE: :wake_reason,
-
HTTP_USER_AGENT: :user_agent,
-
HTTP_WIDTH: :width
-
}.freeze
-
-
# Models the HTTP headers for quick access to attributes.
-
1
Model = Struct.new(*KEY_MAP.values) do
-
1
using Refinements::Hash
-
-
1
def self.for headers, key_map: KEY_MAP
-
32
headers.transform_keys(key_map).then { new(**it) }
-
end
-
-
1
def initialize(**)
-
21
super
-
21
freeze
-
end
-
-
1
def device_attributes
-
{
-
12
battery_charge:,
-
battery_voltage:,
-
firmware_version: firmware_version.to_s,
-
wake_reason:,
-
wifi:,
-
width:,
-
height:
-
}.compress
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "pipeable"
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Firmware
-
1
module Headers
-
# Parses firmware HTTP headers into records.
-
1
class Parser
-
1
include Deps[
-
:logger,
-
model_name_transformer: "aspects.firmware.headers.transformers.model_name",
-
sensors_transformer: "aspects.firmware.headers.transformers.sensors"
-
]
-
1
include Pipeable
-
-
1
using Refinements::Hash
-
-
1
def initialize(schema: Schemas::Firmware::Header, model: Model, **)
-
20
@schema = schema
-
20
@model = model
-
20
super(**)
-
end
-
-
1
def call headers
-
40
logger.debug(tags: tags(headers)) { "Processing device request headers." }
-
-
20
pipe headers,
-
validate(schema, as: :to_h),
-
use(model_name_transformer),
-
use(sensors_transformer),
-
to(model, :for)
-
end
-
-
1
private
-
-
1
attr_reader :schema, :model
-
-
1
def tags(headers) = [headers.slice(*schema.key_map.map(&:name)).symbolize_keys]
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "initable"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Firmware
-
1
module Headers
-
1
module Transformers
-
# Transforms a model name to a name that looked up in the database.
-
1
class ModelName
-
1
include Deps[:logger]
-
-
1
include Initable[
-
key: :HTTP_MODEL,
-
map: {
-
"og" => "og_plus",
-
"reTerminal E1001" => "seeed_e1001",
-
"reTerminal E1002" => "seeed_e1002",
-
"seeed_esp32c3" => "seeed_e1001",
-
"seeed_esp32s3" => "seeed_e1002",
-
"waveshare" => "waveshare_4_26",
-
"x" => "v2",
-
"xiao_epaper_display" => "og_plus",
-
"XTEINK_X4" => "xteink_x4"
-
},
-
fallback: "og_plus"
-
]
-
-
1
include Dry::Monads[:result]
-
-
1
def call headers
-
60
rename(headers[key]).bind { |value| Success headers.merge!(key => value) }
-
end
-
-
1
private
-
-
1
def rename original
-
30
value = String map[original]
-
-
30
else: 6
then: 24
return Success value unless value.empty?
-
-
6
logger.debug do
-
6
"Unknown name when transforming #{key} header: #{original.inspect}. " \
-
"Using fallback: #{fallback}."
-
end
-
-
6
Success fallback
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "dry/monads"
-
1
require "initable"
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Firmware
-
1
module Headers
-
1
module Transformers
-
# Transforms sensors header into an array of records.
-
1
class Sensors
-
1
include Initable[
-
key: :HTTP_SENSORS,
-
source: "device",
-
delimiters: {line: ",", attribute: ";", pair: "="}
-
]
-
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::Hash
-
-
1
def call headers
-
20
content = String headers[key]
-
-
20
then: 14
else: 6
return split_lines content, headers if content.include? pair_delimiter
-
-
6
Success headers.merge!(key => Core::EMPTY_ARRAY)
-
end
-
-
1
private
-
-
1
def split_lines content, headers
-
14
content.split(line_delimiter)
-
15
.map { split_attributes it }
-
14
.then { Success headers.merge! key => it }
-
end
-
-
1
def split_attributes line
-
15
line.split(attribute_delimiter)
-
85
.to_h { it.split pair_delimiter }
-
.merge!(source:)
-
.symbolize_keys!
-
14
.transform_value!(:created_at) { Time.at it.to_i }
-
end
-
-
1
def line_delimiter = delimiters.fetch :line
-
-
1
def attribute_delimiter = delimiters.fetch :attribute
-
-
1
def pair_delimiter = delimiters.fetch :pair
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "initable"
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Firmware
-
# Transforms a raw firmware log into attributes fit for creating a device log record.
-
1
class LogTransformer
-
1
include Initable[key_map: {id: :external_id}.freeze]
-
-
1
using Refinements::Hash
-
-
1
def call payload
-
5
payload.fetch(:logs, Core::EMPTY_HASH).map do |item|
-
6
item.transform_keys!(key_map).transform_value!(:created_at) { Time.at it }
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Firmware
-
1
module Models
-
# Models data for API setup responses.
-
1
Setup = Struct.new :api_key, :friendly_id, :image_url, :message do
-
1
def self.for device
-
3
new api_key: device.api_key,
-
friendly_id: device.friendly_id,
-
image_url: %(#{Hanami.app[:settings].api_uri}/assets/setup.bmp),
-
message: "Welcome to Terminus!"
-
end
-
-
1
def initialize(**)
-
7
super
-
7
self[:message] ||= "MAC Address not registered."
-
7
freeze
-
end
-
-
1
def to_json(*) = to_h.to_json(*)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Firmware
-
# A firmware attachment synchronizer with Core server.
-
1
class Synchronizer
-
1
include Deps[:trmnl_api, "aspects.downloader", repository: "repositories.firmware"]
-
1
include Dry::Monads[:result]
-
-
1
def initialize(struct: Structs::Firmware.new, **)
-
5
@struct = struct
-
5
super(**)
-
end
-
-
1
def call
-
5
result = trmnl_api.latest_firmware
-
-
5
in: 4
case result
-
7
else: 1
in Success(payload) then download(payload).bind { attach it, payload.version }
-
1
else result
-
end
-
end
-
-
1
private
-
-
1
attr_reader :struct
-
-
1
def download(payload) = downloader.call payload.url
-
-
1
def attach response, version
-
3
record = repository.find_by(version:)
-
-
3
then: 1
else: 2
return Success record if record
-
-
2
struct.upload StringIO.new(response), metadata: {"filename" => "#{version}.bin"}
-
2
then: 1
else: 1
struct.valid? ? Success(create(version, struct)) : Failure(struct.errors)
-
end
-
-
1
def create version, struct
-
1
repository.create version: version,
-
kind: "trmnl",
-
attachment_data: struct.attachment_attributes
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/pathname"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Fonts
-
# Synchronizes TRMNL Framework fonts for local use.
-
1
class Synchronizer
-
1
include Deps[:settings, "aspects.downloader"]
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::Pathname
-
-
1
def initialize(
-
configuration_path: Hanami.app.root.join("config/fonts.yml"),
-
root_uri: "https://trmnl.com/fonts",
-
**
-
)
-
5
@configuration_path = configuration_path
-
5
@root_uri = root_uri
-
5
super(**)
-
end
-
-
1
def call
-
5
root = settings.fonts_root.make_dir
-
5
names = YAML.load_file(configuration_path).fetch "names"
-
-
5
delete_unknown_files root, names
-
106
names.map { download_to root, it }
-
end
-
-
1
private
-
-
1
attr_reader :configuration_path, :root_uri
-
-
1
def delete_unknown_files root, names
-
5
root.files
-
3
.map { it.basename.to_s }
-
5
.then { |locals| locals - names }
-
2
.each { root.join(it).delete }
-
end
-
-
1
def download_to root, name
-
101
path = root.join name
-
-
101
then: 1
else: 100
return Success path if path.exist?
-
-
150
downloader.call("#{root_uri}/#{name}").fmap { |response| path.write response.body }
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Jobs
-
# Manages job schedules.
-
1
class Schedule
-
1
include Deps[:sidekiq]
-
-
1
using Refinements::Hash
-
-
1
def upsert name, configuration = Dry::EMPTY_HASH, old_name: nil
-
20
then: 1
else: 19
return if identical? name, configuration
-
-
19
then: 9
if configuration.empty?
-
9
delete name
-
else: 10
else
-
10
then: 1
else: 9
delete old_name if old_name && old_name != name
-
10
sidekiq.set_schedule name, configuration
-
end
-
end
-
-
1
def delete(name) = sidekiq.remove_schedule name
-
-
1
private
-
-
1
def identical? name, configuration
-
20
[name, sidekiq.get_schedule(name)].hash == [name, configuration.stringify_keys].hash
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "functionable"
-
1
require "json"
-
-
1
module Terminus
-
1
module Aspects
-
# A simple JSON pretty printer.
-
1
module JSONFormatter
-
1
extend Functionable
-
-
1
def call data
-
90
in: 62
case data
-
62
in: 27
in nil | Core::EMPTY_ARRAY | Core::EMPTY_HASH then Core::EMPTY_STRING
-
27
else: 1
in Array | Hash then JSON data, indent: " ", space: " ", object_nl: "\n", array_nl: "\n"
-
1
else fail TypeError, "Unknown type to format as JSON for: #{data.inspect}."
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Logging
-
# Adapts Cogger Rack middleware for provider registration.
-
1
module RackAdapter
-
1
module_function
-
-
1
def with logger
-
5
@logger ||= logger
-
5
self
-
end
-
-
1
def new application
-
4
@application = Cogger::Rack::Logger.new application, {logger: @logger}
-
end
-
-
1
def call(environment) = @application.call environment
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Models
-
# Clones an existing model.
-
1
class Cloner
-
1
include Deps[repository: "repositories.model"]
-
1
include Dry::Monads[:result]
-
-
1
def call(id, **overrides)
-
6
original = repository.find id
-
6
attributes = {label: "#{original.label} Clone", name: "#{original.name}_clone"}
-
-
6
Success create(original, attributes, overrides)
-
rescue ROM::SQL::UniqueConstraintError => error
-
3
build_failure error.message
-
end
-
-
1
private
-
-
1
def create original, attributes, overrides
-
6
repository.create(
-
**original.to_h.except(:id, :created_at, :updated_at),
-
**attributes,
-
**overrides
-
)
-
end
-
-
1
def build_failure message
-
3
match = message.match(/Key \((?<key>[^)]+)\)/)
-
3
Failure match[:key].to_sym => ["must be unique"]
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Models
-
1
DEFAULTS = {
-
mime_type: "image/png",
-
colors: 2,
-
bit_depth: 1,
-
rotation: 0,
-
offset_x: 0,
-
offset_y: 0,
-
scale_factor: 1,
-
width: 0,
-
height: 0,
-
css: nil
-
}.freeze
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Models
-
# Finds model record by model or device ID.
-
1
class Finder
-
1
include Deps[
-
model_repository: "repositories.model",
-
device_repository: "repositories.device"
-
]
-
-
1
include Dry::Monads[:result]
-
-
1
def call model_id: nil, device_id: nil
-
108
model = find model_id, device_id
-
-
108
then: 79
else: 29
return Success model if model
-
-
29
Failure "Unable to find model for model ID (#{model_id.inspect}) or " \
-
"device ID (#{device_id.inspect})."
-
end
-
-
1
private
-
-
1
def find model_id, device_id
-
108
else: 9
then: 99
return model_repository.find model_id unless device_id
-
-
9
device = device_repository.find device_id
-
9
then: 8
else: 1
model_repository.find device.model_id if device
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Models
-
# Builds palette selections for use as HTML select options.
-
1
class PaletteOptioner
-
1
include Deps[
-
join_repository: "repositories.model_palette",
-
palette_repository: "repositories.palette"
-
]
-
-
1
def call model = nil, prompt: "Select..."
-
28
then: 13
else: 1
load_restricted(model).then { it.empty? ? load_all : it }
-
.reduce [[prompt, ""]] do |all, palette|
-
5
all.append [palette.label, palette.id]
-
end
-
end
-
-
1
private
-
-
1
def load_restricted model
-
14
else: 9
then: 5
return Core::EMPTY_ARRAY unless model
-
-
9
join_repository.where(model_id: model.id)
-
.map(&:palette_id)
-
9
.then { |ids| palette_repository.where id: ids }
-
end
-
-
1
def load_all = palette_repository.all
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "initable"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Models
-
# A models synchronizer with Core server.
-
1
class Synchronizer
-
1
include Deps[
-
:trmnl_api,
-
model_repository: "repositories.model",
-
palette_repository: "repositories.palette",
-
join_repository: "repositories.model_palette"
-
]
-
-
1
include Initable[kinds: %w[byod kindle tidbyt trmnl]]
-
1
include Dry::Monads[:result]
-
-
1
def call
-
15
result = trmnl_api.models
-
-
15
case result
-
in: 14
in Success(*payload)
-
14
delete payload.map(&:name)
-
24
else: 1
payload.each { |item| process item, palette_repository.all }
-
1
else result
-
end
-
end
-
-
1
private
-
-
1
def delete remote_names
-
14
locals = model_repository.where kind: kinds
-
14
local_names = locals.map(&:name)
-
-
14
model_repository.delete_all kind: kinds, name: local_names - remote_names
-
end
-
-
1
def process item, palettes
-
10
attributes = item.to_h
-
10
names = attributes[:palette_names]
-
10
model = upsert item, attributes
-
-
10
add_missing_palettes names, palettes, model
-
10
set_default_palette model, names
-
end
-
-
1
def upsert item, attributes
-
10
record = model_repository.find_by name: item.name
-
-
10
then: 7
if record
-
7
model_repository.update(record.id, **attributes)
-
else: 3
else
-
3
model_repository.create(**attributes)
-
end
-
end
-
-
# :reek:TooManyStatements
-
1
def add_missing_palettes names, all, model
-
10
model_id = model.id
-
28
required_ids = all.select { names.include? it.name }
-
.map(&:id)
-
10
existing_ids = join_repository.where(model_id:).map(&:palette_id)
-
-
10
(required_ids - existing_ids).each do |palette_id|
-
10
join_repository.create model_id:, palette_id:
-
end
-
-
10
model
-
end
-
-
1
def set_default_palette model, names
-
10
then: 3
else: 7
return if model.default_palette_id
-
-
7
palette = palette_repository.find_by name: names.last
-
-
7
else: 6
then: 1
return unless palette
-
-
6
model_repository.update model.id, default_palette_id: palette.id
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "initable"
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Palettes
-
# A palettes synchronizer with Core server.
-
1
class Synchronizer
-
1
include Deps[:trmnl_api, repository: "repositories.palette"]
-
1
include Initable[default_kind: "trmnl"]
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::Hash
-
-
1
def call
-
5
result = trmnl_api.palettes
-
-
5
case result
-
in: 4
in Success(*payload)
-
4
delete payload.map(&:name)
-
4
else: 1
process payload
-
1
else result
-
end
-
end
-
-
1
private
-
-
1
def delete remote_names
-
4
locals = repository.where kind: default_kind
-
4
local_names = locals.map(&:name)
-
-
4
repository.delete_all kind: default_kind, name: local_names - remote_names
-
end
-
-
1
def process payload
-
7
payload.each { |item| upsert item }
-
4
Success()
-
end
-
-
1
def upsert item
-
3
attributes = transform item
-
3
record = repository.find_by name: item.name
-
-
3
then: 1
if record
-
1
repository.update(record.id, **attributes)
-
else: 2
else
-
2
repository.create(**attributes)
-
end
-
end
-
-
4
def transform(item) = item.to_h.then { {**it, kind: default_kind} }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "bcrypt"
-
1
require "initable"
-
-
1
module Terminus
-
1
module Aspects
-
# Creates passwords with computation cost respective of environment.
-
1
class PasswordEncryptor
-
1
include Initable[
-
password: BCrypt::Password,
-
minimum: BCrypt::Engine::MIN_COST,
-
maximum: BCrypt::Engine::DEFAULT_COST
-
]
-
-
1
def call text, environment: Hanami.env
-
123
then: 122
else: 1
cost = environment == :test ? minimum : maximum
-
123
password.create text, cost:
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Playlists
-
# Clones an existing playlist.
-
1
class Cloner
-
1
include Deps[
-
repository: "repositories.playlist",
-
item_repository: "repositories.playlist_item"
-
]
-
1
include Dry::Monads[:result]
-
-
1
def call(id, **overrides)
-
8
original = repository.with_items.by_pk(id).one
-
8
attributes = {label: "#{original.label} Clone", name: "#{original.name}_clone"}
-
-
8
Success create(attributes, overrides, original)
-
rescue ROM::SQL::UniqueConstraintError => error
-
3
build_failure error.message
-
end
-
-
1
private
-
-
1
def create attributes, overrides, original
-
8
repository.create(attributes.merge!(overrides)).tap do |clone|
-
5
add_associations clone, original
-
end
-
end
-
-
1
def add_associations clone, original
-
5
cloned_items = add_items clone, original
-
10
then: 3
else: 2
curent_screen_id = original.current_item.then { it.screen_id if it }
-
-
5
else: 3
then: 2
return unless curent_screen_id
-
-
3
add_current_item clone, cloned_items, curent_screen_id
-
end
-
-
1
def add_items clone, original
-
5
original.playlist_items.each.with_index 1 do |item, position|
-
8
item_repository.create playlist_id: clone.id, **item.cloneable_attributes, position:
-
end
-
end
-
-
1
def add_current_item clone, cloned_items, curent_screen_id
-
9
cloned_items.find { |item| item.screen_id == curent_screen_id }
-
.then do |current_item|
-
3
repository.update clone.id, current_item_id: current_item.id
-
end
-
end
-
-
1
def build_failure message
-
3
match = message.match(/Key \((?<key>[^)]+)\)/)
-
3
Failure match[:key].to_sym => ["must be unique"]
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Playlists
-
# Creates list of screen options for selection within a HTML select element.
-
1
class ScreenOptioner
-
1
include Deps[repository: "repositories.screen"]
-
-
1
def call prompt: "Select..."
-
3
repository.all.reduce [[prompt, nil]] do |all, screen|
-
2
all.append ["#{screen.label} - #{screen.model.label}", screen.id]
-
end
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "refinements/array"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Playlists
-
# The playlist slideshow window of current item and associated slides.
-
1
class SlideWindow
-
1
include Deps[repository: "repositories.playlist"]
-
-
1
using Refinements::Array
-
-
1
attr_reader :playlist
-
-
1
def initialize(playlist, **)
-
18
super(**)
-
18
@playlist = playlist
-
end
-
-
1
def item = playlist.current_item
-
-
1
def screens id = nil
-
15
enumerable = playlist.screens.ring
-
-
15
else: 8
then: 7
return enumerable.first unless item
-
-
8
enumerable.find do |before, current, after|
-
17
then: 8
else: 9
break before, current, after if current.id == (id || item.screen_id)
-
end
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "functionable"
-
1
require "petail"
-
1
require "refinements/array"
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Aspects
-
# Builds details for global problems.
-
1
module ProblemDetail
-
1
extend Functionable
-
-
1
using Refinements::Array
-
1
using Refinements::Hash
-
-
1
def duplicate message, instance
-
3
key, value = message.match(/Key \((?<key>[^)]+)\)=\((?<value>[^)]+)\)/m)
-
.named_captures
-
.values_at "key", "value"
-
-
3
Petail[
-
type: "/problem_details#duplicate_value",
-
status: :conflict,
-
detail: "#{key.capitalize} must be unique. " \
-
"Please use a value other than #{value.inspect}.",
-
instance:
-
]
-
end
-
-
1
def enum message, instance
-
2
key, value = message.match(/"(?<value>.+?)".+:(?<key>.+?)\s/)
-
.named_captures
-
.values_at "key", "value"
-
2
allowed = JSON(message[/\[".+?"\]/m]).to_usage :or
-
-
2
Petail[
-
type: "/problem_details#invalid_enum",
-
status: :unprocessable_content,
-
detail: "Invalid value for #{key}: #{value.inspect}. Use: #{allowed}.",
-
instance:
-
]
-
end
-
-
1
def foreign_key message, instance
-
2
key, value = message.match(/Key \((?<key>[^)]+)\)=\((?<value>[^)]+)\)/m)
-
.named_captures
-
.values_at "key", "value"
-
-
2
Petail[
-
type: "/problem_details#invalid_foreign_key",
-
status: :unprocessable_content,
-
detail: "Invalid `#{key}` value: #{value}. Does not exist.",
-
instance:
-
]
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/array"
-
1
require "sanitize"
-
-
1
module Terminus
-
1
module Aspects
-
# A custom HTML sanitizer.
-
1
class Sanitizer
-
1
using Refinements::Array
-
-
1
def initialize configuration_path: Hanami.app.root.join("config/sanitize.yml"),
-
defaults: Sanitize::Config::RELAXED,
-
client: Sanitize
-
239
@configuration_path = configuration_path
-
239
@defaults = defaults
-
239
@client = client
-
end
-
-
1
def call(content) = client.document content, configuration
-
-
1
private
-
-
1
attr_reader :configuration_path, :defaults, :client
-
-
1
def configuration = client::Config.merge(defaults, elements:, attributes:)
-
-
1
def elements
-
93
defaults[:elements].including YAML.load_file(configuration_path).fetch("elements")
-
end
-
-
1
def attributes
-
93
defaults[:attributes].merge YAML.load_file(configuration_path).fetch("attributes")
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
# Converts to greyscale image based on MIME Type.
-
1
class Converter
-
1
include Deps["aspects.screens.converters.color", "aspects.screens.converters.monochrome"]
-
-
63
then: 1
else: 61
def call(mold) = mold.color? ? color.call(mold) : monochrome.call(mold)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
1
module Converters
-
# Converts to color image.
-
1
class Color
-
1
include Deps[mini_magick: "mini_magick.core"]
-
1
include Dry::Monads[:result]
-
-
1
def call mold
-
6
convert mold
-
rescue MiniMagick::Error => error
-
1
Failure error.message
-
end
-
-
1
private
-
-
1
def convert mold
-
6
output_path = mold.output_path
-
24
colors = mold.color_codes.map { "xc:#{it}" }
-
-
6
mini_magick.convert do |tool|
-
5
tool << mold.input_path.to_s
-
5
then: 1
else: 4
tool.rotate mold.rotation if mold.rotatable?
-
5
tool.resize "#{mold.dimensions}!"
-
5
then: 1
else: 4
tool.crop mold.crop if mold.cropable?
-
5
tool.normalize
-
5
tool.modulate "110,150"
-
5
tool.colorspace "RGB"
-
5
tool.merge! [
-
"(",
-
"-size",
-
"1x1",
-
*colors,
-
"+append",
-
"+write",
-
"mpr:palette",
-
"+delete",
-
")"
-
]
-
5
tool.dither "FloydSteinberg"
-
5
tool.remap "mpr:palette"
-
5
tool.colorspace "sRGB"
-
5
tool << "#{mold.file_type}:#{output_path}"
-
end
-
-
5
Success output_path
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
1
module Converters
-
# Converts to monochrome image.
-
1
class Monochrome
-
1
include Deps[mini_magick: "mini_magick.core"]
-
1
include Dry::Monads[:result]
-
-
1
def call mold
-
74
route mold
-
rescue MiniMagick::Error => error
-
1
Failure error.message
-
end
-
-
1
private
-
-
1
def route mold
-
74
in: 3
case mold
-
3
in: 2
in bit_depth: 1, mode: "dither" then as_one_bit_dither mold
-
2
in: 1
in bit_depth: 2..4, mode: "dither" then as_two_to_four_bit_dither mold
-
1
in: 65
in bit_depth: 8, mode: "dither" then as_eight_bit_dither mold
-
65
in: 2
in bit_depth: 1 then as_one_bit mold
-
2
else: 1
in bit_depth: 2..8 then as_two_to_eight_bit mold
-
1
else Failure "Unsupported monochrome bit depth: #{mold.bit_depth}."
-
end
-
end
-
-
1
def as_one_bit_dither mold
-
3
convert mold do |tool|
-
3
tool.dither "FloydSteinberg"
-
3
tool.remap "pattern:gray50"
-
end
-
end
-
-
1
def as_two_to_four_bit_dither mold
-
2
convert mold do |tool|
-
2
tool.colorspace "Gray"
-
2
tool.dither "FloydSteinberg"
-
2
tool.posterize mold.grays
-
end
-
end
-
-
1
def as_eight_bit_dither mold
-
1
convert mold do |tool|
-
1
tool.type "Grayscale"
-
end
-
end
-
-
1
def as_one_bit mold
-
65
convert mold do |tool|
-
64
tool.monochrome
-
64
tool.colors mold.colors
-
end
-
end
-
-
1
def as_two_to_eight_bit mold
-
2
convert mold do |tool|
-
2
tool.colorspace "Gray"
-
2
tool.dither "None"
-
2
tool.posterize mold.grays
-
end
-
end
-
-
1
def convert mold
-
73
output_path = mold.output_path
-
-
73
mini_magick.convert do |tool|
-
72
tool << mold.input_path.to_s
-
72
then: 1
else: 71
tool.rotate mold.rotation if mold.rotatable?
-
72
tool.resize "#{mold.dimensions}!"
-
72
then: 1
else: 71
tool.crop mold.crop if mold.cropable?
-
72
yield tool
-
72
tool.alpha "off"
-
72
tool.depth mold.bit_depth
-
72
tool.strip
-
72
tool << "#{mold.file_type}:#{output_path}"
-
end
-
-
72
Success output_path
-
end
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "initable"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
1
module Designer
-
# Renders device preview image event streams.
-
1
class EventStream
-
1
include Deps[:assets, :logger, repository: "repositories.screen"]
-
1
include Initable[%i[req name], kernel: Kernel]
-
-
1
def each
-
5
kernel.loop do
-
5
yield <<~CONTENT
-
event: preview
-
data: #{load_screen}
-
-
CONTENT
-
-
5
kernel.sleep 1
-
end
-
end
-
-
1
private
-
-
1
def load_screen
-
5
repository.find_by(name:).then do |screen|
-
5
then: 3
else: 2
screen ? render_preview(screen) : render_loader
-
end
-
end
-
-
1
def render_preview screen
-
3
width, height = screen.image_attributes[:metadata].values_at :width, :height
-
3
path = screen.image_uri
-
-
3
debug path
-
3
%(<img src="#{path}" alt="Preview" class="image" width="#{width}" height="#{height}"/>)
-
end
-
-
1
def render_loader
-
2
path = assets["loader.svg"].path
-
-
2
debug path
-
2
%(<img src="#{path}" alt="Loader" class="image" width="800" height="480"/>)
-
end
-
-
1
def debug path
-
10
logger.debug { "Streaming: #{path}." }
-
end
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "initable"
-
-
1
require_relative "event_stream"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
1
module Designer
-
# Streams Server Side Events (SSE) for device screen previews.
-
1
class Middleware
-
1
include Initable[
-
%i[req application],
-
%i[keyreq pattern],
-
headers: {
-
"Content-Encoding" => "identity",
-
"Content-Type" => "text/event-stream",
-
"Cache-Control" => "no-cache",
-
"X-Accel-Buffering" => "no"
-
},
-
event_stream: EventStream
-
]
-
-
1
def call environment
-
306
request = Rack::Request.new environment
-
306
path = request.path
-
-
306
in: 2
case path.match pattern
-
2
else: 304
in name: then [200, headers, event_stream.new(name)]
-
304
else application.call environment
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
# Fetches a device's current screen.
-
1
class Fetcher
-
1
include Deps[
-
"aspects.screens.sleeper",
-
playlist_repository: "repositories.playlist",
-
playlist_item_repository: "repositories.playlist_item"
-
]
-
1
include Dry::Monads[:result]
-
-
1
def call device
-
24
then: 1
if device.asleep?
-
1
sleeper.call device
-
else: 23
else
-
31
find_playlist(device.playlist_id).bind { |playlist| find_current_item playlist }
-
.fmap(&:screen)
-
end
-
end
-
-
1
private
-
-
1
def find_playlist id
-
23
playlist = playlist_repository.find id
-
-
23
then: 8
else: 15
return Success playlist if playlist
-
-
15
Failure "Unable to fetch screen. Can't find playlist with ID: #{id.inspect}."
-
end
-
-
1
def find_current_item playlist
-
8
id = playlist.current_item_id
-
8
item = playlist_item_repository.find id
-
-
8
then: 7
else: 1
return Success item if item
-
-
1
Failure "Unable to fetch screen. Can't find current playlist item with ID: #{id.inspect}."
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "initable"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
# Findos or creates record with image attachment from HTML content only.
-
1
class FindOrCreator
-
1
include Deps[
-
"aspects.screens.temp_pather",
-
"aspects.screens.mold_builder",
-
repository: "repositories.screen"
-
]
-
1
include Initable[struct: proc { Terminus::Structs::Screen.new }]
-
1
include Dry::Monads[:result]
-
-
1
def call(**parameters)
-
32
mold_builder.call(**parameters).bind do |mold|
-
32
record = find mold
-
32
then: 3
else: 29
record ? Success(record) : create(mold)
-
end
-
end
-
-
1
private
-
-
1
def find(mold) = repository.find_by name: mold.name, model_id: mold.model_id
-
-
1
def create mold
-
29
temp_pather.call mold do |path|
-
29
Success repository.create_with_image(path, mold, struct)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
# Creates error with problem details for device.
-
1
class Gaffer
-
1
include Deps["aspects.screens.upserter", view: "views.screens.gaffe.new"]
-
-
1
def call device, message
-
4
upserter.call model_id: device.model_id,
-
content: String.new(view.call(body: message)),
-
**device.screen_attributes("error")
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
# Defines the blueprint in which to create a screen.
-
1
Mold = Struct.new(
-
:model_id,
-
:name,
-
:label,
-
:content,
-
:mode,
-
:mime_type,
-
:bit_depth,
-
:colors,
-
:color_codes,
-
:grays,
-
:rotation,
-
:offset_x,
-
:offset_y,
-
:width,
-
:height,
-
:input_path,
-
:output_path
-
) do
-
1
def color? = dither? && Array(color_codes).any?
-
-
1
def crop = "#{dimensions}+#{offset_x}+#{offset_y}"
-
-
1
def cropable? = !offset_x.zero? || !offset_y.zero?
-
-
1
def dither? = mode == "dither"
-
-
1
def dimensions = "#{width}x#{height}"
-
-
1
def file_name = %(#{name}.#{mime_type.split("/").last})
-
-
80
then: 1
else: 78
def file_type = mime_type.split("/").last.then { it.match?(/bmp/i) ? "bmp3" : it }
-
-
1
def image? = mime_type.start_with? "image"
-
-
1
def image_attributes = {model_id:, name:, label:}
-
-
1
def rotatable? = !rotation.zero?
-
-
1
def viewport = {width:, height:}
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "initable"
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
# Initializes and builds a screen mold.
-
1
class MoldBuilder
-
1
include Deps["aspects.models.finder", :logger, palette_repository: "repositories.palette"]
-
1
include Initable[mold: Mold, fallbacks: {grays: 0, color_codes: []}]
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::Hash
-
-
1
def call model_id: nil, device_id: nil, **attributes
-
65
finder.call(model_id:, device_id:)
-
61
.fmap { |model| palette_attributes_for model }
-
61
.fmap { |model, palette| build model, palette, attributes }
-
61
.fmap { log_debug it }
-
end
-
-
1
private
-
-
1
def palette_attributes_for model
-
61
palette = palette_repository.find model.default_palette_id
-
61
then: 1
else: 60
attributes = palette ? palette.screen_attributes : fallbacks
-
-
61
[model, attributes]
-
end
-
-
1
def build model, palette_attributes, attributes
-
61
allowed_keys = mold.members
-
-
61
mold.new(
-
**model.to_h.transform_keys!(id: :model_id).slice(*allowed_keys),
-
**palette_attributes,
-
**attributes.slice(*allowed_keys)
-
)
-
end
-
-
1
def log_debug record
-
122
logger.debug(tags: [record.to_h]) { "Screen mold built." }
-
61
record
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "wholeable"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
# A fallback (null object) for times when you need a screen that behaves like one but isn't.
-
1
class Placeholder
-
1
include Wholeable[:id, :label, :name, :uri, :width, :height]
-
1
include Deps[:assets]
-
-
1
def initialize(
-
id: 0,
-
label: "Placeholder",
-
name: "placeholder",
-
uri: "setup.svg",
-
width: 800,
-
height: 480,
-
**
-
)
-
169
@id = id
-
169
@label = label
-
169
@name = name
-
169
@uri = uri
-
169
@width = width
-
169
@height = height
-
169
super(**)
-
end
-
-
1
def image_uri = assets[uri].path
-
-
1
def popover_attributes = {id:, label:, uri: image_uri, width:, height:}
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
# Updates a device's current playlist item by rotating to next screen.
-
1
class Rotator
-
1
include Deps[
-
"aspects.screens.sleeper",
-
playlist_repository: "repositories.playlist",
-
item_repository: "repositories.playlist_item"
-
]
-
1
include Dry::Monads[:result]
-
-
1
def call device
-
13
then: 1
if device.asleep?
-
1
sleeper.call device
-
else: 12
else
-
21
find_playlist(device.playlist_id).fmap { |playlist| advance_current_item playlist }
-
9
.bind { |item| obtain_screen item }
-
end
-
end
-
-
1
private
-
-
1
def find_playlist id
-
12
playlist = playlist_repository.find id
-
-
12
then: 9
else: 3
return Success playlist if playlist
-
-
3
Failure "Unable to obtain next screen. Can't find playlist with ID: #{id.inspect}."
-
end
-
-
# :reek:FeatureEnvy
-
1
def advance_current_item playlist
-
9
then: 1
else: 8
return playlist.current_item if playlist.manual?
-
-
8
item_repository.next_item(after: playlist.current_item_position, playlist_id: playlist.id)
-
8
.tap { |item| playlist_repository.update_current_item playlist, item }
-
end
-
-
1
def obtain_screen item
-
9
then: 8
else: 1
return Success item.screen if item
-
-
1
Failure "Unable to obtain next screen. Playlist has no items."
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "ferrum"
-
1
require "refinements/pathname"
-
1
require "refinements/string"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
# Saves web page as screenshot.
-
1
class Shoter
-
1
include Deps[:settings, :logger]
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::Pathname
-
1
using Refinements::String
-
-
1
OPTIONS = {
-
"disable-dev-shm-usage" => nil,
-
"disable-gpu" => nil,
-
"hide-scrollbar" => nil,
-
"no-sandbox" => nil
-
}.freeze
-
-
1
def initialize(browser: Ferrum::Browser, options: OPTIONS, **)
-
218
super(**)
-
218
@browser = browser
-
218
@settings = settings.browser.merge! browser_options: options
-
end
-
-
1
def call(content, output_path, **viewport) = save content, viewport, output_path
-
-
1
private
-
-
1
attr_reader :settings, :browser
-
-
1
def save content, viewport, output_path
-
72
instance = browser.new settings
-
-
69
Pathname.mktmpdir do |work_dir|
-
69
instance.create_page
-
58
instance.set_viewport(**viewport)
-
58
instance.main_frame.content = work_dir.join("content.html").write(content).read
-
58
instance.network.wait_for_idle duration: 1
-
58
instance.screenshot path: output_path.to_s
-
58
instance.quit
-
end
-
-
58
Success output_path
-
3
rescue Ferrum::BrowserError => error then handle_browser_error instance, error
-
2
rescue Ferrum::DeadBrowserError => error then handle_dead_browser_error error
-
4
rescue Ferrum::TimeoutError => error then handle_timeout_error instance, error
-
3
rescue Ferrum::NoSuchTargetError => error then handle_no_such_target_error instance, error
-
2
rescue Ferrum::ProcessTimeoutError => error then handle_process_timeout_error error
-
end
-
-
1
def handle_browser_error instance, error
-
3
instance.quit
-
6
logger.debug { "Screen shoter has browser error: #{error.message}" }
-
-
3
Failure "Unable to capture screenshot due to an instance error such as " \
-
"page navigation, element interaction, or something else."
-
end
-
-
1
def handle_dead_browser_error error
-
4
logger.debug { "Screen shoter has dead browser: #{error.message}" }
-
-
2
Failure "Unable to capture screenshot due to a dead browser. " \
-
"This could mean the browser crashed, server is out of memory, " \
-
"or a resource limitation has been hit."
-
end
-
-
1
def handle_timeout_error instance, error
-
4
then: 3
else: 1
instance.quit if instance
-
8
logger.debug { "Screen shoter has timeout: #{error.message}" }
-
-
4
seconds = settings.fetch :timeout, 0
-
-
4
Failure "Unable to capture screenshot due to timming out after " \
-
+ %(#{seconds} #{"second".pluralize "s"}. ) \
-
+ "This might have happened due to the page taking a long time to load."
-
end
-
-
1
def handle_no_such_target_error instance, error
-
3
instance.quit
-
6
logger.debug { "Screen shoter has no such target: #{error.message}" }
-
3
Failure "Unable to capture screenshot because the page closed or crashed."
-
end
-
-
1
def handle_process_timeout_error error
-
4
logger.debug { "Screen shoter has process timeout: #{error.message}" }
-
-
2
seconds = settings.fetch :process_timeout, 0
-
-
2
Failure "Unable to capture screenshot because the browser could not produce a " \
-
+ %(websocket URL within #{seconds} #{"second".pluralize "s"}.)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
# Creates sleep screen for new device.
-
1
class Sleeper
-
1
include Deps[creator: "aspects.screens.find_or_creator", view: "views.screens.sleep.new"]
-
-
1
def call device
-
4
creator.call model_id: device.model_id,
-
content: String.new(view.call(device:)),
-
**device.screen_attributes("sleep")
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "inspectable"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
# Saves content as image to temporary file path for optional processing.
-
1
class TempPather
-
1
include Deps["aspects.sanitizer", "aspects.screens.shoter", "aspects.screens.converter"]
-
1
include Dry::Monads[:result]
-
1
include Inspectable[sanitizer: :type]
-
-
57
def call(mold, &) = Pathname.mktmpdir { process mold, it, & }
-
-
1
private
-
-
1
def process mold, directory
-
56
mold.output_path = directory.join mold.file_name
-
-
112
capture_input(mold, directory).bind { converter.call mold }
-
56
then: 54
else: 2
.bind { |path| block_given? ? yield(path) : path }
-
end
-
-
1
def capture_input mold, directory
-
56
content = sanitizer.call mold.content
-
-
56
shoter.call(content, directory.join("input.png"), **mold.viewport)
-
56
.fmap { |path| mold.input_path = path }
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
# Creates or updates a screen.
-
1
class Upserter
-
1
include Deps[
-
"aspects.screens.mold_builder",
-
"aspects.screens.upserters.html",
-
"aspects.screens.upserters.preprocessed",
-
"aspects.screens.upserters.unprocessed"
-
]
-
1
include Dry::Monads[:result]
-
-
1
def call **parameters
-
32
in: 26
case parameters
-
26
in label:, name:, content: then handle_html label:, name:, content:, **parameters
-
in: 2
in label:, name:, uri:, preprocessed: true
-
2
in: 2
handle_preprocessed label:, name:, content: uri, **parameters
-
2
else: 2
in label:, name:, uri: then handle_unprocessed label:, name:, content: uri, **parameters
-
2
else Failure "Invalid parameters: #{parameters.inspect}."
-
end
-
end
-
-
1
private
-
-
23
def handle_html(**) = mold_builder.call(**).bind { html.call it }
-
-
3
def handle_unprocessed(**) = mold_builder.call(**).bind { unprocessed.call it }
-
-
3
def handle_preprocessed(**) = mold_builder.call(**).bind { preprocessed.call it }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "initable"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
1
module Upserters
-
# Creates screen record with image attachment from HTML content.
-
1
class HTML
-
1
include Deps["aspects.screens.temp_pather", repository: "repositories.screen"]
-
1
include Initable[struct: proc { Terminus::Structs::Screen.new }]
-
1
include Dry::Monads[:result]
-
-
1
def call mold
-
23
temp_pather.call mold do |path|
-
23
Success repository.upsert_with_image(path, mold, struct)
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
1
module Upserters
-
# Creates screen record with image attachment from preprocesed image URI.
-
1
class Preprocessed
-
1
include Deps["mini_magick.image", repository: "repositories.screen"]
-
1
include Dry::Monads[:result]
-
-
1
def initialize(struct: Terminus::Structs::Screen.new, **)
-
48
@struct = struct
-
48
super(**)
-
end
-
-
4
def call(mold) = Pathname.mktmpdir { process mold, it }
-
-
1
private
-
-
1
attr_reader :struct
-
-
1
def process mold, directory
-
3
path = Pathname(directory).join "input.png"
-
6
image.open(mold.content).write(path).then { save mold, path }
-
end
-
-
1
def save(mold, path) = Success repository.upsert_with_image(path, mold, struct)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "refinements/struct"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
1
module Upserters
-
# Creates screen record with image attachment from unprocessed image URI.
-
1
class Unprocessed
-
1
include Deps[
-
"mini_magick.image",
-
"aspects.screens.converter",
-
repository: "repositories.screen"
-
]
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::Struct
-
-
1
def initialize(struct: Terminus::Structs::Screen.new, **)
-
48
@struct = struct
-
48
super(**)
-
end
-
-
4
def call(mold) = Pathname.mktmpdir { process mold, it }
-
-
1
private
-
-
1
attr_reader :struct
-
-
1
def process mold, directory
-
3
mold.with! input_path: Pathname(directory).join("input.png"),
-
output_path: directory.join(mold.file_name)
-
-
3
image.open(mold.content)
-
.write(mold.input_path)
-
3
.then { converter.call mold }
-
3
.bind { |path| save mold, path }
-
end
-
-
1
def save(mold, path) = Success repository.upsert_with_image(path, mold, struct)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Screens
-
# Creates welcome screen for new device.
-
1
class Welcomer
-
1
include Deps[creator: "aspects.screens.find_or_creator", view: "views.screens.welcome.new"]
-
-
1
def call device
-
26
creator.call model_id: device.model_id,
-
content: String.new(view.call(device:)),
-
**device.screen_attributes("welcome")
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Aspects
-
1
module Users
-
# Validates and creates a new user complete with account and membership.
-
1
class Creator
-
1
include Deps[
-
"aspects.password_encryptor",
-
contract: "contracts.users.create",
-
repository: "repositories.user",
-
password_relation: "relations.user_password_hash",
-
account_repository: "repositories.account",
-
membership_relation: "relations.membership"
-
]
-
1
include Dry::Monads[:result]
-
-
1
DEFAULTS = {name: "default", label: "Default"}.freeze
-
-
1
def initialize(defaults: DEFAULTS, **)
-
9
@defaults = defaults
-
9
super(**)
-
end
-
-
1
def call(**attributes)
-
9
result = contract.call(attributes).to_monad
-
-
9
then: 2
else: 7
return result if result.failure?
-
-
7
Success create_user(attributes[:user], attributes.fetch(:account, {}))
-
end
-
-
1
private
-
-
1
attr_reader :defaults
-
-
1
def create_user user_attributes, account_attributes
-
7
account_attributes = defaults.merge account_attributes
-
7
password = user_attributes.delete :password
-
-
7
repository.create(**user_attributes).tap do |user|
-
7
password_relation.insert id: user.id, password_hash: password_encryptor.call(password)
-
7
create_membership user, account_attributes
-
end
-
end
-
-
1
def create_membership user, account_attributes
-
7
account_repository.find_or_create(**account_attributes).then do |account|
-
7
membership_relation.insert account_id: account.id, user_id: user.id
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/string"
-
-
1
module Terminus
-
1
module Aspects
-
1
module Users
-
# Validates and updates an existing user.
-
1
class Updater
-
1
include Deps[
-
"aspects.password_encryptor",
-
contract: "contracts.users.update",
-
repository: "repositories.user",
-
password_relation: "relations.user_password_hash"
-
]
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::String
-
-
1
def call(**attributes)
-
7
result = contract.call(attributes).to_monad
-
-
7
then: 2
else: 5
return result if result.failure?
-
-
5
Success update(attributes[:id], attributes[:user])
-
end
-
-
1
private
-
-
1
def update id, attributes
-
5
password = attributes.delete :password
-
10
user = repository.update(id, **attributes).then { repository.find id }
-
-
5
update_password user, password
-
end
-
-
1
def update_password user, value
-
5
then: 4
else: 1
return user if String(value).blank?
-
-
1
id = user.id
-
-
1
password_relation.by_pk(id).delete
-
1
password_relation.upsert id: id, password_hash: password_encryptor.call(value)
-
1
user
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/pathname"
-
-
1
module Terminus
-
# Defines user create contract.
-
1
class Contract < Dry::Validation::Contract
-
1
using Refinements::Pathname
-
-
1
config.messages.backend = :i18n
-
1
config.messages.load_paths.merge Hanami.app.root.join("config/locales").files("*.yml")
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Contracts
-
1
module Devices
-
# The contract for device creates.
-
1
class Create < Contract
-
2
params { required(:device).filled Schemas::Devices::Upsert }
-
-
1
rule device: :sleep_start_at, &Rules::SleepStartAt
-
1
rule device: :sleep_stop_at, &Rules::SleepStopAt
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Contracts
-
1
module Devices
-
# The contract for device patches.
-
1
class Patch < Contract
-
1
params do
-
1
required(:id).filled :integer
-
1
required(:device).filled Schemas::Devices::Patch
-
end
-
-
1
rule device: :sleep_start_at, &Rules::SleepStartAt
-
1
rule device: :sleep_stop_at, &Rules::SleepStopAt
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Contracts
-
1
module Devices
-
# The contract for device updates.
-
1
class Update < Contract
-
1
params do
-
1
required(:id).filled :integer
-
1
required(:device).filled Schemas::Devices::Upsert
-
end
-
-
1
rule device: :sleep_start_at, &Rules::SleepStartAt
-
1
rule device: :sleep_stop_at, &Rules::SleepStopAt
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Contracts
-
1
module Extensions
-
# The contract for extension creation.
-
1
class Create < Contract
-
1
config.messages.namespace = :extension
-
-
1
params do
-
1
required(:extension).filled Schemas::Extensions::Upsert
-
1
optional(:model_ids).filled :array
-
end
-
-
1
rule extension: :interval, &Rules::Cron
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Contracts
-
1
module Extensions
-
1
module Exchanges
-
# The contract for extension exchange creation.
-
1
class Create < Contract
-
1
params do
-
1
required(:extension_id).filled :integer
-
1
required(:exchange).filled Schemas::Extensions::Exchanges::Upsert
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Contracts
-
1
module Extensions
-
1
module Exchanges
-
# The contract for extension exchange updating.
-
1
class Update < Contract
-
1
params do
-
1
required(:extension_id).filled :integer
-
1
required(:id).filled :integer
-
1
required(:exchange).filled Schemas::Extensions::Exchanges::Upsert
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Contracts
-
1
module Extensions
-
# The contract for extension updates.
-
1
class Update < Contract
-
1
config.messages.namespace = :extension
-
-
1
params do
-
1
required(:id).filled :integer
-
1
required(:extension).filled Schemas::Extensions::Upsert
-
1
optional(:model_ids).filled :array
-
end
-
-
1
rule extension: :interval, &Rules::Cron
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Contracts
-
1
module Models
-
# The contract for model cloning.
-
1
class Clone < Contract
-
1
config.messages.namespace = :model
-
-
1
params do
-
1
required(:model_id).filled :integer
-
1
required(:model).filled Schemas::Models::Upsert
-
end
-
-
1
rule model: :mime_type, &Rules::ImageMimeType
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Contracts
-
1
module Models
-
# The contract for model creation.
-
1
class Create < Contract
-
1
config.messages.namespace = :model
-
-
2
params { required(:model).filled Schemas::Models::Upsert }
-
-
1
rule model: :mime_type, &Rules::ImageMimeType
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Contracts
-
1
module Models
-
# The contract for model updates.
-
1
class Update < Contract
-
1
config.messages.namespace = :model
-
-
1
params do
-
1
required(:id).filled :integer
-
1
required(:model).filled Schemas::Models::Upsert
-
end
-
-
1
rule model: :mime_type, &Rules::ImageMimeType
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Contracts
-
1
module Rules
-
1
Cron = lambda do
-
26
attributes = values.fetch(:extension).slice :interval, :unit
-
-
26
case attributes
-
in {unit: "none"} \
-
| {unit: "minute", interval: 0..59} \
-
| {unit: "hour", interval: 0..23} \
-
| {unit: "day", interval: 1..31} \
-
in: 14
| {unit: "week", interval: 0..6} \
-
14
| {unit: "month", interval: 1..12} then next
-
else: 12
else
-
12
key.failure "invalid schedule for #{attributes[:unit]} #{attributes[:interval]}."
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Contracts
-
1
module Rules
-
1
ImageMimeType = lambda do
-
18
then: 13
else: 5
next if values.dig(:model, :mime_type).start_with? "image/"
-
-
5
key.failure "must be an image"
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Contracts
-
1
module Rules
-
1
SleepStartAt = lambda do
-
27
stop_at = values.dig :device, :sleep_stop_at
-
-
27
then: 4
else: 23
if value && stop_at.nil? then key.failure "must have corresponding stop time"
-
23
then: 5
else: 18
elsif value.nil? && stop_at then key.failure "must be filled"
-
18
else next
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Contracts
-
1
module Rules
-
1
SleepStopAt = lambda do
-
27
start_at = values.dig :device, :sleep_start_at
-
-
27
then: 5
else: 22
if value && start_at.nil? then key.failure "must have corresponding start time"
-
22
then: 4
else: 18
elsif value.nil? && start_at then key.failure "must be filled"
-
18
else next
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Contracts
-
1
module Users
-
# Defines user create contract.
-
1
class Create < Contract
-
1
params do
-
1
required(:user).filled(:hash) do
-
1
required(:name).filled :string
-
1
required(:email).filled :string
-
1
optional(:password).maybe(:string, min_size?: 10)
-
1
optional(:status_id).filled :integer
-
end
-
-
1
optional(:account).filled(:hash) do
-
1
required(:name).filled :string
-
1
required(:label).filled :string
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Contracts
-
1
module Users
-
# Defines user update contract.
-
1
class Update < Contract
-
1
params do
-
1
required(:id).filled :integer
-
-
1
required(:user).filled(:hash) do
-
1
required(:name).filled :string
-
1
required(:email).filled :string
-
1
optional(:password).maybe(:string, min_size?: 10)
-
1
required(:status_id).filled :integer
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "hanami/db/relation"
-
-
1
module Terminus
-
1
module DB
-
# The application database base relation.
-
1
class Relation < Hanami::DB::Relation
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "hanami/db/repo"
-
-
1
module Terminus
-
1
module DB
-
# The application database base repository.
-
1
class Repository < Hanami::DB::Repo
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "hanami/db/struct"
-
-
1
module Terminus
-
1
module DB
-
# The application database base struct.
-
1
class Struct < Hanami::DB::Struct
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "sidekiq"
-
-
1
module Terminus
-
1
module Jobs
-
# The base abstract class for which all jobs inherit from.
-
1
class Base
-
1
include Dry::Monads[:result]
-
1
include Sidekiq::Job
-
-
1
sidekiq_options queue: "within_1_hour"
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "initable"
-
-
1
module Terminus
-
1
module Jobs
-
1
module Batches
-
# Enqueues a job for each model ID.
-
1
class Extension < Base
-
1
include Deps[repository: "repositories.extension"]
-
1
include Initable[job: Jobs::Extensions::Screen]
-
-
1
sidekiq_options queue: "within_1_minute"
-
-
1
def perform id
-
6
extension = repository.find id
-
-
6
else: 4
then: 2
return Failure "Unable to enqueue jobs for extension: #{id}." unless extension
-
-
4
then: 1
else: 3
extension.devices.any? ? enqueue_devices(extension) : enqueue_models(extension)
-
-
4
Success "Enqueued jobs for extension: #{id}."
-
end
-
-
1
private
-
-
1
def enqueue_models extension
-
4
extension.models.each { |model| job.perform_async extension.id, model.id }
-
end
-
-
1
def enqueue_devices extension
-
2
extension.devices.each { |device| job.perform_async extension.id, nil, device.id }
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Jobs
-
1
module Extensions
-
# Refreshes exchange with new responses.
-
1
class ExchangeRefresh < Base
-
1
include Deps[
-
"aspects.extensions.exchanges.refresher",
-
repository: "repositories.extension_exchange"
-
]
-
-
1
sidekiq_options queue: "within_1_minute"
-
-
1
def perform id
-
3
exchange = repository.find id
-
-
3
else: 2
then: 1
return Failure "Unable to find exchange ID: #{id}." unless exchange
-
-
2
refresher.call exchange
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Jobs
-
1
module Extensions
-
# Creates screen for extension and model or device ID.
-
1
class Screen < Base
-
1
include Deps["aspects.extensions.screen_upserter", repository: "repositories.extension"]
-
-
1
sidekiq_options queue: "within_1_minute"
-
-
1
def perform id, model_id = nil, device_id = nil
-
5
extension = repository.find id
-
-
5
else: 4
then: 1
return Failure "Unable to find by extension ID: #{id}." unless extension
-
-
4
screen_upserter.call extension, model_id:, device_id:
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Jobs
-
1
module Synchronizers
-
# Synchronizes TRMNL Firmware for local use.
-
1
class Firmware < Base
-
1
include Deps[:settings, :logger, "aspects.firmware.synchronizer"]
-
-
1
sidekiq_options queue: "within_1_minute"
-
-
1
def perform
-
3
then: 1
else: 2
return synchronizer.call if settings.firmware_synchronizer
-
-
4
logger.info { "Firmware synchronization is disabled." }
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Jobs
-
1
module Synchronizers
-
# Synchronizes TRMNL Framework fonts for local use.
-
1
class Font < Base
-
1
include Deps["aspects.fonts.synchronizer"]
-
-
1
sidekiq_options queue: "within_1_minute"
-
-
1
def perform = synchronizer.call
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Jobs
-
1
module Synchronizers
-
# Synchronizes TRMNL models for local use.
-
1
class Model < Base
-
1
include Deps[
-
:settings,
-
:logger,
-
palette: "aspects.palettes.synchronizer",
-
model: "aspects.models.synchronizer"
-
]
-
-
1
sidekiq_options queue: "within_1_minute"
-
-
1
def perform
-
7
then: 5
if settings.model_synchronizer
-
7
palette.call.bind { model.call }
-
else: 2
else
-
4
logger.info { "Model synchronization is disabled." }
-
end
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Jobs
-
1
module Synchronizers
-
# Synchronizes server hosted sensor data.
-
1
class Sensor < Base
-
1
include Deps["aspects.devices.sensors.synchronizer"]
-
-
1
sidekiq_options queue: "within_1_minute"
-
-
1
def perform = synchronizer.call
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Providers
-
# The logger provider.
-
1
class Logger < Hanami::Provider::Source
-
3
RESOLVER = proc { Object.const_get "Cogger" }
-
-
1
def initialize(environment: Hanami.env, resolver: RESOLVER, **)
-
9
@environment = environment
-
9
@resolver = resolver
-
9
@id = Hanami.app.namespace.to_s.downcase.to_sym
-
9
super(**)
-
end
-
-
1
def prepare = require "cogger"
-
-
1
def start
-
8
add_filters
-
8
register :logger, build_instance
-
end
-
-
1
private
-
-
1
attr_reader :environment, :resolver, :id
-
-
1
def add_filters
-
8
cogger.add_filters :api_key,
-
:csrf,
-
:HTTP_ACCESS_TOKEN,
-
:HTTP_ID,
-
:mac_address,
-
:password,
-
:password_confirmation
-
end
-
-
1
def build_instance
-
8
io = "log/#{environment}.log"
-
-
8
case environment
-
when: 5
when :test
-
5
when: 2
cogger.new(id:, io: StringIO.new, formatter: :json, level: :debug).add_stream io:
-
2
else: 1
when :development then cogger.new(id:).add_stream(io:, formatter: :json)
-
1
else cogger.new id:, formatter: :json
-
end
-
end
-
-
1
def cogger
-
16
@cogger ||= resolver.call
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Providers
-
# The Sidekiq provider.
-
1
class Sidekiq < Hanami::Provider::Source
-
1
include Deps[:logger]
-
-
2
RESOLVER = proc { Object.const_get "Sidekiq" }
-
-
1
def initialize(resolver: RESOLVER, **)
-
4
@resolver = resolver
-
4
super(**)
-
end
-
-
1
def prepare
-
2
require "sidekiq"
-
2
require "sidekiq-scheduler"
-
2
require "yaml"
-
end
-
-
1
def start
-
3
configure_server
-
3
configure_client
-
3
register :sidekiq, sidekiq
-
end
-
-
1
private
-
-
1
attr_reader :resolver
-
-
1
def configure_client
-
3
sidekiq.configure_client do |configuration|
-
1
configuration.redis = {url: slice[:settings].keyvalue_url}
-
1
configuration.logger = slice[:logger]
-
end
-
end
-
-
1
def configure_server
-
skipped
# :nocov:
-
skipped
sidekiq.configure_server do |configuration|
-
skipped
configuration.redis = {url: slice[:settings].keyvalue_url}
-
skipped
configuration.logger = slice[:logger]
-
skipped
configuration.on(:startup) { load_schedule }
-
skipped
end
-
skipped
# :nocov:
-
end
-
-
1
def sidekiq
-
9
@sidekiq ||= resolver.call
-
end
-
-
1
def load_schedule
-
skipped
# :nocov:
-
skipped
jobs = YAML.load_file slice.root.join("config/sidekiq_scheduler.yml")
-
skipped
-
skipped
jobs.each do |schedule_name, options|
-
skipped
resolver.call.set_schedule schedule_name, options
-
skipped
job_name = options["class"]
-
skipped
Object.const_get(job_name).perform_in 0
-
skipped
rescue NameError, TypeError
-
skipped
logger.error { "Unable to initialize job: #{job_name}." }
-
skipped
end
-
skipped
# :nocov:
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The account relation.
-
1
class Account < DB::Relation
-
1
schema :account, infer: true do
-
2
associations { has_many :memberships, relation: :membership }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The device relation.
-
1
class Device < DB::Relation
-
1
schema :device, infer: true do
-
1
associations do
-
1
belongs_to :model, relation: :model
-
1
belongs_to :playlist, relation: :playlist
-
1
has_many :device_logs, relation: :device_log, as: :logs
-
1
has_many :device_sensors, relation: :device_sensor, as: :sensors
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The device log relation.
-
1
class DeviceLog < DB::Relation
-
1
schema :device_log, infer: true do
-
2
associations { belongs_to :device, relation: :device }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The device sensor relation.
-
1
class DeviceSensor < DB::Relation
-
1
schema :device_sensor, infer: true do
-
2
associations { belongs_to :device, relation: :device }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The extension relation.
-
1
class Extension < DB::Relation
-
1
schema :extension, infer: true do
-
1
associations do
-
1
has_many :extension_devices, relation: :extension_device
-
1
has_many :devices, through: :extension_device, relation: :device, as: :devices
-
1
has_many :extension_models, relation: :extension_model
-
1
has_many :models, through: :extension_model, relation: :model, as: :models
-
1
has_many :extension_exchanges, relation: :extension_exchange
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The extension and device join relation.
-
1
class ExtensionDevice < DB::Relation
-
1
schema :extension_device, infer: true do
-
1
associations do
-
1
belongs_to :extension, relation: :extension
-
1
belongs_to :device, relation: :device
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The extension exchange relation.
-
1
class ExtensionExchange < DB::Relation
-
1
schema :extension_exchange, infer: true do
-
2
associations { belongs_to :extension, relation: :extension }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The extension and model join relation.
-
1
class ExtensionModel < DB::Relation
-
1
schema :extension_model, infer: true do
-
1
associations do
-
1
belongs_to :extension, relation: :extension
-
1
belongs_to :model, relation: :model
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The firmware relation.
-
1
class Firmware < DB::Relation
-
1
schema :firmware, infer: true
-
-
1
def by_version_desc
-
80
order Sequel.desc(Sequel.function(:string_to_array, :version, ".").cast("int[]"))
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The membership relation.
-
1
class Membership < DB::Relation
-
1
schema :membership, infer: true do
-
1
associations do
-
1
belongs_to :account, relation: :account
-
1
belongs_to :user, relation: :user
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The model relation.
-
1
class Model < DB::Relation
-
1
schema :model, infer: true do
-
1
associations do
-
1
belongs_to :default_palette, relation: :palette
-
1
has_many :devices, relation: :device
-
1
has_many :screens, relation: :screen
-
1
has_many :extension_models, relation: :extension_model
-
1
has_many :extensions, through: :extension_model, relation: :extension
-
1
has_many :model_palettes, relation: :model_palette
-
1
has_many :palettes, through: :model_palette, relation: :palette
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The model and palette join relation.
-
1
class ModelPalette < DB::Relation
-
1
schema :model_palette, infer: true do
-
1
associations do
-
1
belongs_to :model, relation: :model
-
1
belongs_to :palette, relation: :palette
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The palette relation.
-
1
class Palette < DB::Relation
-
1
schema :palette, infer: true do
-
1
associations do
-
1
has_many :model_palettes, relation: :model_palette
-
1
has_many :models, through: :model_palette, relation: :model
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The playlist relation.
-
1
class Playlist < DB::Relation
-
1
schema :playlist, infer: true do
-
1
associations do
-
1
belongs_to :current_item, relation: :playlist_item
-
1
has_many :devices, relation: :device
-
1
has_many :playlist_items, relation: :playlist_item, as: :playlist_items, view: :ordered
-
1
has_many :screens,
-
through: :playlist_item,
-
relation: :screen,
-
as: :screens,
-
view: :ordered
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The playlist item relation.
-
1
class PlaylistItem < DB::Relation
-
1
schema :playlist_item, infer: true do
-
1
associations do
-
1
belongs_to :playlist, relation: :playlist
-
1
belongs_to :screen, relation: :screen
-
end
-
end
-
-
1
def ordered = select_append(:position).order :position
-
-
1
def next_item playlist_id:, after:
-
14
scope = combine(:screen).where(playlist_id:).order :position
-
-
28
next_or_previous = scope.where { position > after }
-
.first
-
-
14
next_or_previous || scope.first
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The screen relation.
-
1
class Screen < DB::Relation
-
1
schema :screen, infer: true do
-
1
associations do
-
1
belongs_to :model, relation: :model
-
1
has_many :playlist_items, relation: :playlist_item, as: :playlist_items, view: :ordered
-
1
has_many :playlists, through: :playlist_item, relation: :playlist, as: :playlists
-
end
-
end
-
-
1
def ordered = select_append(playlist_item[:position]).order :position
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The user relation.
-
1
class User < DB::Relation
-
1
schema :user, infer: true do
-
1
associations do
-
1
belongs_to :user_status, relation: :user_status, as: :status
-
1
has_many :memberships, relation: :membership
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The user password hash relation.
-
1
class UserPasswordHash < DB::Relation
-
1
schema :user_password_hash, infer: true
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Relations
-
# The user status relation.
-
1
class UserStatus < DB::Relation
-
1
schema :user_status, infer: true
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Repositories
-
# The account repository.
-
1
class Account < DB::Repository[:account]
-
1
commands :create, delete: :by_pk
-
-
1
commands update: :by_pk,
-
use: :timestamps,
-
plugins_options: {timestamps: {timestamps: :updated_at}}
-
-
1
def all
-
4
account.order { created_at.asc }
-
.to_a
-
end
-
-
4
then: 2
else: 1
def find(id) = (account.by_pk(id).one if id)
-
-
1
def find_by(**) = account.where(**).one
-
-
1
def find_or_create(**) = find_by(**) || create(**)
-
-
1
def search key, value
-
3
account.where(Sequel.ilike(key, "%#{value}%"))
-
3
.order { created_at.asc }
-
.to_a
-
end
-
-
1
def where(**)
-
4
account.where(**)
-
4
.order { created_at.asc }
-
.to_a
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Repositories
-
# The device repository.
-
1
class Device < DB::Repository[:device]
-
1
commands :create, delete: :by_pk
-
-
1
commands update: :by_pk,
-
use: :timestamps,
-
plugins_options: {timestamps: {timestamps: :updated_at}}
-
-
1
def all
-
58
with_associations.order { created_at.asc }
-
.to_a
-
end
-
-
33
then: 31
else: 1
def find(id) = (with_associations.by_pk(id).one if id)
-
-
1
def find_by(**) = with_associations.where(**).one
-
-
1
def mirror_playlist ids, playlist_id
-
7
device.update playlist_id: Sequel.case({{id: ids} => playlist_id}, nil)
-
end
-
-
1
def search key, value
-
7
device.combine(:model)
-
.where(Sequel.ilike(key, "%#{value}%"))
-
7
.order { created_at.asc }
-
.to_a
-
end
-
-
1
def update_by_mac_address(value, **attributes)
-
13
device = find_by mac_address: value
-
-
13
then: 2
else: 11
return device if attributes.empty?
-
11
else: 8
then: 3
return unless device
-
-
8
update device.id, **attributes
-
end
-
-
1
def where(**)
-
3
with_associations.where(**)
-
3
.order { created_at.asc }
-
.to_a
-
end
-
-
1
private
-
-
1
def with_associations = device.combine :model, :playlist
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Repositories
-
# The device log repository.
-
1
class DeviceLog < DB::Repository[:device_log]
-
1
commands :create, delete: :by_pk
-
-
1
commands update: :by_pk,
-
use: :timestamps,
-
plugins_options: {timestamps: {timestamps: :updated_at}}
-
-
1
def all
-
5
device_log.combine(:device)
-
5
.order { created_at.desc }
-
.to_a
-
end
-
-
8
then: 6
else: 1
def find(id) = (device_log.combine(:device).by_pk(id).one if id)
-
-
1
def delete_by_device(device_id, id) = device_log.where(device_id:, id:).delete
-
-
1
def delete_all_by_device(device_id) = device_log.where(device_id:).command(:delete).call
-
-
1
def search(key, value, **)
-
6
device_log.where(**)
-
.where(Sequel.ilike(key, "%#{value}%"))
-
6
.order { created_at.asc }
-
.to_a
-
end
-
-
1
def where(**)
-
8
device_log.where(**)
-
8
.order { created_at.desc }
-
.to_a
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Repositories
-
# The device sensor repository.
-
1
class DeviceSensor < DB::Repository[:device_sensor]
-
1
commands :create, delete: :by_pk
-
-
1
def all
-
22
with_associations.order { created_at.desc }
-
.to_a
-
end
-
-
4
then: 2
else: 1
def find(id) = (with_associations.by_pk(id).one if id)
-
-
1
def find_by(**) = with_associations.where(**).one
-
-
1
def limited_where(max = 25, **)
-
12
device_sensor.where(**)
-
.limit(max)
-
12
.order { created_at.desc }
-
.to_a
-
end
-
-
1
def search(key, value, **)
-
4
with_associations.where(**)
-
.where(Sequel.ilike(key, "%#{value}%"))
-
4
.order { created_at.asc }
-
.to_a
-
end
-
-
1
def where(**)
-
44
with_associations.where(**)
-
44
.order { created_at.desc }
-
.to_a
-
end
-
-
1
private
-
-
1
def with_associations = device_sensor.combine :device
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Repositories
-
# The extension repository.
-
1
class Extension < DB::Repository[:extension]
-
1
commands :create, delete: :by_pk
-
-
1
commands update: :by_pk,
-
use: :timestamps,
-
plugins_options: {timestamps: {timestamps: :updated_at}}
-
-
1
def all
-
26
extension.order { created_at.asc }
-
.to_a
-
end
-
-
1
def create_with_devices attributes, device_ids
-
4
transaction do
-
4
record = create attributes
-
-
4
create_associations :extension_device, record, :device_id, device_ids
-
2
record
-
end
-
end
-
-
1
def create_with_models attributes, model_ids
-
25
transaction do
-
25
record = create attributes
-
-
21
create_associations :extension_model, record, :model_id, model_ids
-
19
record
-
end
-
end
-
-
88
then: 83
else: 4
def find(id) = (with_associations.by_pk(id).one if id)
-
-
1
def find_by(**) = with_associations.where(**).one
-
-
1
def search key, value
-
7
extension.where(Sequel.ilike(key, "%#{value}%"))
-
7
.order { created_at.asc }
-
.to_a
-
end
-
-
1
def update_with_devices id, attributes, device_ids
-
19
transaction do
-
19
record = update id, attributes
-
-
19
update_associations :extension_device, id, :device_id, device_ids
-
19
record
-
end
-
end
-
-
1
def update_with_models id, attributes, model_ids
-
7
transaction do
-
7
record = update id, attributes
-
-
7
update_associations :extension_model, id, :model_id, model_ids
-
7
record
-
end
-
end
-
-
1
def where(**)
-
4
extension.where(**)
-
4
.order { created_at.asc }
-
.to_a
-
end
-
-
1
private
-
-
1
def with_associations = extension.combine :devices, :models
-
-
# rubocop:todo Metrics/ParameterLists
-
1
def create_associations name, record, foreign_key, values
-
36
associations = values.map { |id| {extension_id: record.id, foreign_key => id} }
-
25
__send__(name).changeset(:create, associations).commit
-
end
-
# rubocop:enable Metrics/ParameterLists
-
-
# :reek:FeatureEnvy
-
# :reek:TooManyStatements
-
# rubocop:todo Metrics/ParameterLists
-
1
def update_associations name, id, foreign_key, values
-
26
association = __send__ name
-
-
26
association.where(extension_id: id).exclude(foreign_key => values).delete
-
-
26
old_ids = association.where(extension_id: id, foreign_key => values).map(foreign_key)
-
40
new_ids = values.reject { |id| old_ids.include? id.to_i }
-
40
associations = new_ids.map { |model_id| {extension_id: id, foreign_key => model_id} }
-
-
26
association.changeset(:create, associations).commit
-
end
-
# rubocop:enable Metrics/ParameterLists
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Repositories
-
# The extension device repository.
-
1
class ExtensionDevice < DB::Repository[:extension_device]
-
1
commands :create, delete: :by_pk
-
-
1
commands update: :by_pk,
-
use: :timestamps,
-
plugins_options: {timestamps: {timestamps: :updated_at}}
-
-
1
def all
-
12
extension_device.order { created_at.asc }
-
.to_a
-
end
-
-
4
then: 2
else: 1
def find(id) = (extension_device.by_pk(id).one if id)
-
-
1
def find_by(**) = extension_device.where(**).one
-
-
1
def where(**)
-
5
extension_device.where(**)
-
5
.order { created_at.asc }
-
.to_a
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Repositories
-
# The extension exchange repository.
-
1
class ExtensionExchange < DB::Repository[:extension_exchange]
-
1
commands :create, delete: :by_pk
-
-
1
commands update: :by_pk,
-
use: :timestamps,
-
plugins_options: {timestamps: {timestamps: :updated_at}}
-
-
1
def all
-
8
extension_exchange.order { created_at.asc }
-
.to_a
-
end
-
-
13
then: 11
else: 1
def find(id) = (extension_exchange.by_pk(id).one if id)
-
-
1
def find_by(**) = extension_exchange.where(**).one
-
-
1
def where(**)
-
63
extension_exchange.where(**)
-
63
.order { created_at.asc }
-
.to_a
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Repositories
-
# The extension model repository.
-
1
class ExtensionModel < DB::Repository[:extension_model]
-
1
commands :create, delete: :by_pk
-
-
1
commands update: :by_pk,
-
use: :timestamps,
-
plugins_options: {timestamps: {timestamps: :updated_at}}
-
-
1
def all
-
12
extension_model.order { created_at.asc }
-
.to_a
-
end
-
-
4
then: 2
else: 1
def find(id) = (extension_model.by_pk(id).one if id)
-
-
1
def find_by(**) = extension_model.where(**).one
-
-
1
def where(**)
-
5
extension_model.where(**)
-
5
.order { created_at.asc }
-
.to_a
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Repositories
-
# The firmware repository.
-
1
class Firmware < DB::Repository[:firmware]
-
1
include Deps[:shrine]
-
-
1
commands :create
-
-
1
commands update: :by_pk,
-
use: :timestamps,
-
plugins_options: {timestamps: {timestamps: :updated_at}}
-
-
1
def all = firmware.by_version_desc.to_a
-
-
1
def delete id
-
16
then: 6
else: 2
find(id).then { it.attachment_destroy if it }
-
-
8
firmware.by_pk(id).delete
-
end
-
-
1
def delete_all
-
8
firmware.where { attachment_data.has_key "id" }
-
4
.select { attachment_data.get_text("id").as(:attachment_id) }
-
.map(:attachment_id)
-
1
.each { shrine.storages[:store].delete it }
-
-
4
firmware.delete
-
end
-
-
28
then: 26
else: 1
def find(id) = (firmware.by_pk(id).one if id)
-
-
1
def find_by(**) = firmware.where(**).one
-
-
1
def latest = all.first
-
-
1
def search key, value
-
8
firmware.where(Sequel.like(key, "%#{value}%"))
-
8
.order { created_at.asc }
-
.to_a
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Repositories
-
# The model repository.
-
1
class Model < DB::Repository[:model]
-
1
commands :create, delete: :by_pk
-
-
1
commands update: :by_pk,
-
use: :timestamps,
-
plugins_options: {timestamps: {timestamps: :updated_at}}
-
-
1
def all
-
122
with_associations.order { label.asc }
-
.to_a
-
end
-
-
1
def delete_all(**) = model.where(**).delete
-
-
132
then: 105
else: 26
def find(id) = (with_associations.by_pk(id).one if id)
-
-
1
def find_by(**) = with_associations.where(**).one
-
-
1
def find_or_create(key, value, **)
-
2
with_associations.where(key => value)
-
.one
-
2
.then { |record| record || create(name: value, **) }
-
end
-
-
1
def search key, value
-
7
with_associations.where(Sequel.ilike(key, "%#{value}%"))
-
7
.order { created_at.asc }
-
.to_a
-
end
-
-
1
def where(**)
-
18
with_associations.where(**)
-
18
.order { created_at.asc }
-
.to_a
-
end
-
-
1
private
-
-
1
def with_associations = model.combine(:default_palette)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Repositories
-
# The model palette repository.
-
1
class ModelPalette < DB::Repository[:model_palette]
-
1
commands :create, delete: :by_pk
-
-
1
commands update: :by_pk,
-
use: :timestamps,
-
plugins_options: {timestamps: {timestamps: :updated_at}}
-
-
1
def all
-
4
model_palette.order { created_at.asc }
-
.to_a
-
end
-
-
4
then: 2
else: 1
def find(id) = (model_palette.by_pk(id).one if id)
-
-
1
def find_by(**) = model_palette.where(**).one
-
-
1
def where(**)
-
31
model_palette.where(**)
-
31
.order { created_at.asc }
-
.to_a
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Repositories
-
# The palette repository.
-
1
class Palette < DB::Repository[:palette]
-
1
commands :create, delete: :by_pk
-
-
1
commands update: :by_pk,
-
use: :timestamps,
-
plugins_options: {timestamps: {timestamps: :updated_at}}
-
-
1
def all
-
60
palette.order { label.asc }
-
.to_a
-
end
-
-
1
def delete_all(**) = palette.where(**).delete
-
-
65
then: 3
else: 61
def find(id) = (palette.by_pk(id).one if id)
-
-
1
def find_by(**) = palette.where(**).one
-
-
1
def search key, value
-
3
palette.where(Sequel.ilike(key, "%#{value}%"))
-
3
.order { label.asc }
-
.to_a
-
end
-
-
1
def where(**)
-
22
palette.where(**)
-
22
.order { label.asc }
-
.to_a
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Repositories
-
# The playlist repository.
-
1
class Playlist < DB::Repository[:playlist]
-
1
commands :create, delete: :by_pk
-
-
1
commands update: :by_pk,
-
use: :timestamps,
-
plugins_options: {timestamps: {timestamps: :updated_at}}
-
-
1
def all
-
44
with_current_item.order { created_at.asc }
-
.to_a
-
end
-
-
1
def create_with_items attributes, collection
-
11
transaction do
-
11
record = create attributes
-
11
items = create_items record, collection
-
-
9
then: 8
else: 1
collection.any? ? update(record.id, current_item_id: items.first.id) : record
-
end
-
end
-
-
91
then: 71
else: 19
def find(id) = (with_current_item.by_pk(id).one if id)
-
-
1
def find_by(**) = with_current_item.where(**).one
-
-
1
def search key, value
-
7
playlist.where(Sequel.ilike(key, "%#{value}%"))
-
7
.order { created_at.asc }
-
.to_a
-
end
-
-
1
def update_current_item record, item
-
13
record_id = record.id
-
-
13
then: 11
else: 2
update record_id, current_item_id: item.id if item
-
13
find record_id
-
end
-
-
1
def update_with_items id, attributes, collection
-
8
transaction do
-
8
record = update id, attributes
-
-
8
then: 6
else: 2
playlist_item.where(playlist_id: id).command(:delete).call if collection
-
8
then: 6
else: 2
create_items record, collection if collection
-
8
record
-
end
-
end
-
-
1
def where(**)
-
4
playlist.where(**)
-
4
.order { created_at.asc }
-
.to_a
-
end
-
-
1
def with_items = with_current_item.combine :playlist_items
-
-
1
def with_screens = with_current_item.combine :screens
-
-
1
private
-
-
1
def with_current_item = playlist.combine current_item: :screen
-
-
1
def create_items playlist, collection
-
17
id = playlist.id
-
-
17
collection.map.with_index 1 do |item, position|
-
16
playlist_item.command(:create).call playlist_id: id,
-
screen_id: item[:screen_id],
-
position:
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Repositories
-
# The playlist repository.
-
1
class PlaylistItem < DB::Repository[:playlist_item]
-
1
commands :create, delete: :by_pk
-
-
1
commands update: :by_pk,
-
use: :timestamps,
-
plugins_options: {timestamps: {timestamps: :updated_at}}
-
-
1
def all
-
20
with_associations.order { [playlist_id, position.asc] }
-
.to_a
-
end
-
-
1
def create_with_position(offset: 1, **)
-
29
playlist_item.transaction do
-
29
playlist_item.command(:create)
-
.call(position: playlist_item.count + offset, **)
-
29
.then { find it.id }
-
end
-
end
-
-
1
def delete_all(**) = playlist_item.where(**).delete
-
-
41
then: 38
else: 2
def find(id) = (with_associations.by_pk(id).one if id)
-
-
1
def find_by(**) = with_associations.where(**).one
-
-
1
def next_item(playlist_id:, after:) = playlist_item.next_item(playlist_id:, after:)
-
-
1
def where(**)
-
16
with_associations.where(**)
-
16
.order { [playlist_id, position.asc] }
-
.to_a
-
end
-
-
1
private
-
-
1
def with_associations = playlist_item.combine :playlist, :screen
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/core"
-
1
require "dry/monads"
-
-
1
module Terminus
-
1
module Repositories
-
# The screen repository.
-
1
class Screen < DB::Repository[:screen]
-
1
include Dry::Monads[:result]
-
-
1
commands :create
-
-
1
commands update: :by_pk,
-
use: :timestamps,
-
plugins_options: {timestamps: {timestamps: :updated_at}}
-
-
1
def all
-
28
with_associations.order { updated_at.desc }
-
.to_a
-
end
-
-
1
def create_with_image path, mold, struct
-
110
path.open { |io| struct.upload io, metadata: {"filename" => mold.file_name} }
-
55
create image_data: struct.image_attributes, **mold.image_attributes
-
end
-
-
1
def delete id
-
14
then: 6
else: 1
find(id).then { it.image_destroy if it }
-
7
screen.by_pk(id).delete
-
end
-
-
30
then: 28
else: 1
def find(id) = (with_associations.by_pk(id).one if id)
-
-
1
def find_by(**) = with_associations.where(**).one
-
-
1
def search key, value
-
7
with_associations.where(Sequel.ilike(key, "%#{value}%"))
-
7
.order { created_at.asc }
-
.to_a
-
end
-
-
1
def upsert_with_image path, mold, struct
-
31
record = find_by name: mold.name, model_id: mold.model_id
-
31
then: 6
else: 25
record ? update_with_image(path, mold, record) : create_with_image(path, mold, struct)
-
end
-
-
1
def where(**)
-
4
with_associations.where(**)
-
4
.order { created_at.asc }
-
.to_a
-
end
-
-
1
private
-
-
1
def with_associations = screen.combine :model
-
-
1
def update_with_image path, mold, record
-
12
path.open { |io| record.replace io, metadata: {"filename" => mold.file_name} }
-
6
update record.id, image_data: record.image_attributes, **mold.image_attributes
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Repositories
-
# The user repository.
-
1
class User < DB::Repository[:user]
-
1
commands :create
-
-
1
commands update: :by_pk,
-
use: :timestamps,
-
plugins_options: {timestamps: {timestamps: :updated_at}}
-
-
1
def all
-
16
with_status.order { created_at.asc }
-
.to_a
-
end
-
-
16
then: 13
else: 2
def find(id) = (with_status.by_pk(id).one if id)
-
-
1
def find_by(**) = with_status.where(**).one
-
-
1
def search key, value
-
7
with_status.where(Sequel.ilike(key, "%#{value}%"))
-
7
.order { created_at.asc }
-
.to_a
-
end
-
-
1
def where(**)
-
4
with_status.where(**)
-
4
.order { created_at.asc }
-
.to_a
-
end
-
-
1
private
-
-
1
def with_status = user.combine :status
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Repositories
-
# The user repository.
-
1
class UserStatus < DB::Repository[:user_status]
-
1
def all = user_status.to_a
-
-
4
then: 2
else: 1
def find(id) = (user_status.by_pk(id).one if id)
-
-
1
def find_by(**) = user_status.where(**).one
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Schemas
-
# Coerces a key's value to an empty array when key is missing.
-
1
module Coercers
-
1
using Refinements::Hash
-
-
1
DefaultToArray = lambda do |key, result, default = Core::EMPTY_ARRAY|
-
24
attributes = Hash result.to_h
-
24
else: 12
then: 12
attributes[key] = default unless result.key? key
-
-
24
attributes
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Schemas
-
# Coerces a key's value to false when key is missing.
-
1
module Coercers
-
1
using Refinements::Hash
-
-
1
DefaultToFalse = lambda do |key, result|
-
49
else: 47
then: 2
return unless result.output
-
-
47
attributes = Hash result.to_h
-
47
else: 40
then: 7
attributes[key] = false unless result.key? key
-
-
47
attributes
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "json"
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Schemas
-
# Coerces a key's JSON value into a hash.
-
1
module Coercers
-
1
using Refinements::Hash
-
-
1
JSONToHash = lambda do |key, result|
-
112
attributes = Hash result.to_h
-
214
then: 53
else: 49
attributes.transform_value!(key) { JSON it if it }
-
rescue JSON::ParserError
-
1
attributes
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Schemas
-
# Coerces a key's line delimited string value into an array.
-
1
module Coercers
-
1
using Refinements::Hash
-
-
1
LinesToArray = lambda do |key, result|
-
57
Hash(result.to_h).transform_value!(key) { String(it).split(/\r\n|\n|\r|\s/) }
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "json"
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Schemas
-
# Coerces key's URI query parameters value into a hash.
-
1
module Coercers
-
1
using Refinements::Hash
-
-
1
URIQueryToHash = lambda do |key, result|
-
13
Hash(result.to_h).transform_value!(key) do |value|
-
11
else: 5
then: 6
Rack::Utils.parse_query value unless String(value).empty?
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Schemas
-
1
module Devices
-
# Defines device patch schema.
-
1
Patch = Dry::Schema.Params do
-
1
optional(:model_id).filled :integer
-
1
optional(:playlist_id).filled :integer
-
1
optional(:label).filled :string
-
1
optional(:friendly_id).filled :string
-
1
optional(:mac_address).filled Types::MACAddress
-
1
optional(:api_key).filled :string
-
1
optional(:refresh_rate).filled :integer, gt?: 0
-
1
optional(:image_timeout).filled :integer, gteq?: 0
-
1
optional(:firmware_update).filled :bool
-
1
optional(:firmware_version).filled Types::Version
-
1
optional(:battery_charge).filled :float, gteq?: 0
-
1
optional(:battery_voltage).filled :float, gteq?: 0
-
1
optional(:wifi).filled :integer
-
1
optional(:width).filled :integer
-
1
optional(:height).filled :integer
-
1
optional(:wake_reason).filled :string
-
1
optional(:sleep_start_at).maybe :string
-
1
optional(:sleep_stop_at).maybe :string
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Schemas
-
1
module Devices
-
1
module Sensors
-
# Defines device sensor upsert schema.
-
1
Upsert = Dry::Schema.Params do
-
1
required(:make).filled :string
-
1
required(:model).filled :string
-
1
required(:kind).filled :string
-
1
required(:value).filled :float
-
1
required(:unit).filled :string
-
1
required(:created_at).filled :integer
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Schemas
-
1
module Devices
-
# Defines device upsert schema.
-
1
Upsert = Dry::Schema.Params do
-
1
required(:model_id).filled :integer
-
1
required(:playlist_id).maybe :integer
-
1
optional(:label).filled :string
-
1
optional(:friendly_id).filled :string
-
1
optional(:mac_address).filled Types::MACAddress
-
1
optional(:api_key).filled :string
-
1
optional(:refresh_rate).filled :integer, gt?: 0
-
1
optional(:image_timeout).filled :integer, gteq?: 0
-
1
optional(:firmware_update).filled :bool
-
1
optional(:firmware_version).filled Types::Version
-
1
optional(:battery_charge).filled :float, gteq?: 0
-
1
optional(:battery_voltage).filled :float
-
1
optional(:wifi).filled :integer
-
1
optional(:width).filled :integer
-
1
optional(:height).filled :integer
-
1
optional(:wake_reason).filled :string
-
1
optional(:sleep_start_at).maybe :string
-
1
optional(:sleep_stop_at).maybe :string
-
-
1
after(:value_coercer, &Coercers::DefaultToFalse.curry[:firmware_update])
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Schemas
-
1
module Extensions
-
1
module Exchanges
-
# Defines extension exchange upsert schema.
-
1
Upsert = Dry::Schema.Params do
-
1
optional(:headers).maybe :hash
-
1
optional(:verb).filled :string
-
1
required(:template).filled :string
-
1
optional(:body).maybe :hash
-
-
1
after(:value_coercer, &Coercers::JSONToHash.curry[:headers])
-
1
after(:value_coercer, &Coercers::JSONToHash.curry[:body])
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Schemas
-
1
module Extensions
-
# Defines extension upsert schema.
-
1
Upsert = Dry::Schema.Params do
-
1
optional(:model_ids).filled :array
-
1
optional(:device_ids).filled :array
-
1
required(:name).filled :string
-
1
required(:label).filled :string
-
1
required(:description).maybe :string
-
1
optional(:mode).filled :string
-
1
required(:kind).filled :string
-
1
required(:tags).maybe :array
-
1
required(:static_body).maybe :hash
-
1
required(:template).maybe :string
-
1
required(:fields).maybe :array
-
1
required(:data).maybe :hash
-
1
required(:interval).maybe :integer
-
1
optional(:unit).filled :string
-
1
optional(:days).maybe :array
-
1
required(:last_day_of_month).filled :bool
-
1
required(:start_at).filled :date_time
-
-
1
after(:value_coercer, &Coercers::LinesToArray.curry[:tags])
-
1
after(:value_coercer, &Coercers::DefaultToFalse.curry[:last_day_of_month])
-
1
after(:value_coercer, &Coercers::DefaultToArray.curry[:days])
-
1
after(:value_coercer, &Coercers::JSONToHash.curry[:static_body])
-
1
after(:value_coercer, &Coercers::JSONToHash.curry[:fields])
-
1
after(:value_coercer, &Coercers::JSONToHash.curry[:data])
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Schemas
-
1
module Firmware
-
# Validates request headers.
-
1
Header = Dry::Schema.Params do
-
1
optional(:HTTP_ACCESS_TOKEN).maybe :string
-
1
optional(:HTTP_BATTERY_VOLTAGE).filled :float
-
1
optional(:HTTP_FW_VERSION).filled Types::Version
-
1
optional(:HTTP_HEIGHT).filled :integer
-
1
optional(:HTTP_HOST).filled :string
-
1
required(:HTTP_ID).filled Types::MACAddress
-
1
optional(:HTTP_MODEL).filled :string
-
1
optional(:HTTP_PERCENT_CHARGED).filled :float
-
1
optional(:HTTP_REFRESH_RATE).filled :integer
-
1
optional(:HTTP_RSSI).filled :integer
-
1
optional(:HTTP_SENSORS).maybe :string
-
1
optional(:HTTP_UPDATE_SOURCE).filled :string
-
1
optional(:HTTP_USER_AGENT).filled :string
-
1
optional(:HTTP_WIDTH).filled :integer
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Schemas
-
1
module Models
-
# Defines model upsert schema.
-
1
Upsert = Dry::Schema.Params do
-
1
required(:name).filled :string
-
1
required(:label).filled :string
-
1
optional(:description).maybe :string
-
1
optional(:default_palette_id).maybe :integer
-
1
optional(:mime_type).filled :string
-
1
optional(:colors).filled :integer
-
1
optional(:bit_depth).filled :integer
-
1
optional(:rotation).filled :integer
-
1
optional(:offset_x).filled :integer
-
1
optional(:offset_y).filled :integer
-
1
optional(:scale_factor).filled :float
-
1
optional(:width).filled :integer
-
1
optional(:height).filled :integer
-
1
optional(:css).maybe :hash
-
-
1
after(:value_coercer, &Coercers::JSONToHash.curry[:css])
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Serializers
-
# A device serializer for specific keys.
-
1
class Device
-
1
KEYS = %i[
-
id
-
model_id
-
playlist_id
-
friendly_id
-
label
-
mac_address
-
api_key
-
firmware_version
-
wifi
-
battery_charge
-
battery_voltage
-
refresh_rate
-
image_timeout
-
wake_reason
-
width
-
height
-
firmware_update
-
sleep_start_at
-
sleep_stop_at
-
created_at
-
updated_at
-
synced_at
-
].freeze
-
-
1
def initialize record, keys: KEYS, transformer: Transformers::Time
-
8
@record = record
-
8
@keys = keys
-
8
@transformer = transformer
-
end
-
-
1
def to_h
-
8
attributes = record.to_h.slice(*keys)
-
8
attributes.transform_values!(&transformer)
-
8
attributes
-
end
-
-
1
private
-
-
1
attr_reader :record, :keys, :transformer
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Serializers
-
# A model serializer for specific keys.
-
1
class Firmware
-
1
KEYS = %i[id version kind created_at updated_at].freeze
-
-
1
def initialize record, keys: KEYS, transformer: Transformers::Time
-
8
@record = record
-
8
@keys = keys
-
8
@transformer = transformer
-
end
-
-
1
def to_h
-
8
attributes = record.to_h.slice(*keys).merge(file_name: "#{record.version}.bin")
-
8
attributes.transform_values!(&transformer)
-
8
then: 7
else: 1
attributes.merge! metadata, uri: record.attachment_uri if record.attachment_id
-
8
attributes
-
end
-
-
1
private
-
-
1
attr_reader :record, :keys, :transformer
-
-
1
def metadata = record.attachment_attributes[:metadata].slice :mime_type, :size
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Serializers
-
# A model serializer for specific keys.
-
1
class Model
-
1
KEYS = %i[
-
default_palette_id
-
id
-
name
-
label
-
description
-
kind
-
mime_type
-
colors
-
bit_depth
-
rotation
-
offset_x
-
offset_y
-
scale_factor
-
css
-
width
-
height
-
created_at
-
updated_at
-
].freeze
-
-
1
def initialize record, keys: KEYS, transformer: Transformers::Time
-
7
@record = record
-
7
@keys = keys
-
7
@transformer = transformer
-
end
-
-
1
def to_h
-
7
attributes = record.to_h.slice(*keys)
-
7
attributes.transform_values!(&transformer)
-
7
attributes
-
end
-
-
1
private
-
-
1
attr_reader :record, :keys, :transformer
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "initable"
-
-
1
module Terminus
-
1
module Serializers
-
# A playlist serializer for specific keys.
-
1
class Playlist
-
1
include Initable[
-
keys: %i[id name label current_item_id mode created_at updated_at],
-
item_serializer: PlaylistItem
-
]
-
-
1
def initialize(record, transformer: Transformers::Time, **)
-
10
super(**)
-
10
@record = record
-
10
@keys = keys
-
10
@transformer = transformer
-
end
-
-
1
def to_h
-
10
else: 9
then: 1
return Core::EMPTY_HASH unless record
-
-
9
attributes = record.to_h.slice(*keys)
-
9
attributes.transform_values!(&transformer)
-
9
attributes[:items] = items
-
9
attributes
-
end
-
-
1
private
-
-
1
attr_reader :record, :keys, :transformer
-
-
1
def items
-
16
record.playlist_items.map { item_serializer.new(it).to_h }
-
rescue NoMethodError, ROM::Struct::MissingAttribute
-
1
Core::EMPTY_ARRAY
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Serializers
-
# A playlist item serializer for specific keys.
-
1
class PlaylistItem
-
1
KEYS = %i[id screen_id position created_at updated_at].freeze
-
-
1
def initialize record, keys: KEYS, transformer: Transformers::Time
-
8
@record = record
-
8
@keys = keys
-
8
@transformer = transformer
-
end
-
-
1
def to_h
-
8
attributes = record.to_h.slice(*keys)
-
8
attributes.transform_values!(&transformer)
-
8
attributes
-
end
-
-
1
private
-
-
1
attr_reader :record, :keys, :transformer
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Serializers
-
# A screen serializer for specific keys.
-
1
class Screen
-
1
KEYS = %i[id model_id label name created_at updated_at].freeze
-
-
1
def initialize record, keys: KEYS, transformer: Transformers::Time
-
10
@record = record
-
10
@keys = keys
-
10
@transformer = transformer
-
end
-
-
1
def to_h
-
10
attributes = record.to_h.slice(*keys)
-
10
attributes.transform_values!(&transformer)
-
10
then: 9
else: 1
attributes.merge! metadata, uri: record.image_uri if record.image_id
-
10
attributes
-
end
-
-
1
private
-
-
1
attr_reader :record, :keys, :transformer
-
-
1
def metadata
-
9
record.image_attributes[:metadata].slice :filename,
-
:mime_type,
-
:bit_depth,
-
:width,
-
:height,
-
:size
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Serializers
-
1
module Transformers
-
# Transforms SQL time to a string.
-
1
Time = lambda do |value|
-
477
when: 3
case value
-
3
when: 89
when Sequel::SQLTime then value.to_s
-
89
else: 385
when ::Time then value.strftime "%Y-%m-%dT%H:%M:%S%z"
-
385
else value
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Structs
-
# The device struct.
-
1
class Device < DB::Struct
-
1
def as_api_display
-
7
{image_url_timeout: image_timeout, refresh_rate:, update_firmware: firmware_update}
-
end
-
-
1
def asleep? at = Time.now, type: Sequel::SQLTime
-
40
else: 4
then: 36
return false unless sleep_start_at && sleep_stop_at
-
-
4
now = type.create at.hour, at.min, at.sec
-
-
4
then: 2
if sleep_stop_at < sleep_start_at
-
2
now >= sleep_start_at || now <= sleep_stop_at
-
else: 2
else
-
2
(sleep_start_at..sleep_stop_at).cover? now
-
end
-
end
-
-
1
def slug
-
2
else: 1
then: 1
return Core::EMPTY_STRING unless mac_address
-
-
1
mac_address.tr ":", Core::EMPTY_STRING
-
end
-
-
1
def screen_label(prefix) = "#{prefix} #{friendly_id}"
-
-
1
def screen_name(kind) = "terminus_#{kind}_#{friendly_id.downcase}"
-
-
1
def screen_attributes kind
-
{
-
35
model_id:,
-
name: screen_name(kind),
-
label: screen_label(kind.capitalize)
-
}
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Structs
-
# The device sensor struct.
-
1
class DeviceSensor < DB::Struct
-
1
using Refinements::Hash
-
-
1
def liquid_attributes
-
4
{device_id:, make:, model:, kind:, value:, unit:, source:, created_at:}.stringify_keys!
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/core"
-
1
require "refinements/time"
-
-
1
module Terminus
-
1
module Structs
-
# The extension struct.
-
1
class Extension < DB::Struct
-
1
WEEK = %w[sunday monday tuesday wednesday thursday friday saturday].freeze
-
-
1
using Refinements::Time
-
-
1
def export_attributes
-
{
-
4
name:,
-
label:,
-
description:,
-
mode:,
-
kind:,
-
tags:,
-
static_body:,
-
fields:,
-
template:,
-
data:,
-
interval:,
-
unit:,
-
days:,
-
last_day_of_month:,
-
start_at: start_at.rfc_3339
-
}
-
end
-
-
1
def liquid_attributes
-
43
all_fields = Array fields
-
-
43
values = all_fields.each.with_object({}) do |item, all|
-
19
key, value = item.values_at "keyname", "default"
-
19
all[key] = Hash(data).dig("values", key) || value
-
end
-
-
43
{"label" => label, "fields" => all_fields, "values" => values, "data" => data}
-
end
-
-
1
def screen_label = "Extension #{label}"
-
-
1
def screen_name = "extension-#{name}"
-
-
1
def screen_attributes = {label: screen_label, name: screen_name, mode:}
-
-
1
def to_cron croner: Aspects::Croner, week: WEEK
-
14
in: 1
case self
-
3
in unit: "week" then croner.call days.map { week.index it }, unit, time: start_at
-
in: 1
in unit: "month", last_day_of_month: true
-
1
else: 12
croner.call "#{interval}L", unit, time: start_at
-
12
else croner.call interval, unit, time: start_at
-
end
-
end
-
-
1
def to_schedule
-
20
then: 10
else: 10
return [screen_name, Core::EMPTY_HASH] if unit == "none"
-
-
[
-
10
screen_name,
-
{
-
cron: to_cron,
-
class: Terminus::Jobs::Batches::Extension.name,
-
args: [id],
-
description: "The #{label} extension update schedule."
-
}
-
]
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Structs
-
# The extension exchange struct.
-
1
class ExtensionExchange < DB::Struct
-
1
def export_attributes = {headers:, verb:, body:, template:}
-
-
1
def http_attributes = {headers:, verb:, body:}
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Structs
-
# The firmware struct.
-
1
class Firmware < DB::Struct
-
1
include Terminus::Uploaders::Binary::Attachment[:attachment]
-
-
1
using Refinements::Hash
-
-
1
attr_reader :attachment_data
-
-
1
def initialize(*, store: Hanami.app[:shrine].storages[:store])
-
132
super(*)
-
132
@store = store
-
132
@attacher = attachment_attacher
-
end
-
-
1
def attachment_attributes = attributes[:attachment_data].deep_symbolize_keys
-
-
1
def attachment_destroy
-
12
then: 10
else: 2
store.delete attachment_id if attachment_id
-
12
attributes[:attachment_data].clear
-
end
-
-
1
def attachment_id = attachment_attributes[:id]
-
-
1
def attachment_name = attachment_attributes.dig :metadata, :filename
-
-
1
def attachment_open(**)
-
2
io = store.open(attachment_id, **)
-
2
then: 1
else: 1
yield io if block_given?
-
ensure
-
2
io.close
-
end
-
-
1
def attachment_size = attachment_attributes.dig :metadata, :size
-
-
1
def attachment_type = attachment_attributes.dig :metadata, :mime_type
-
-
25
then: 21
else: 3
def attachment_uri(**) = (store.url(attachment_id, **) if attachment_id)
-
-
1
def attach(io, **)
-
12
attacher.assign(io, **).tap { |file| attributes[:attachment_data] = file.data }
-
end
-
-
1
def replace(io, **)
-
4
attachment_destroy
-
4
upload(io, **)
-
4
self
-
end
-
-
1
def upload(io, **)
-
24
attacher.upload(io, **).tap { |file| attributes[:attachment_data] = file.data }
-
end
-
-
1
def errors = attacher.errors
-
-
1
def valid? = errors.empty?
-
-
1
private
-
-
1
attr_reader :attacher, :store
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Structs
-
# The model struct.
-
1
class Model < DB::Struct
-
1
def css_classes
-
19
size = css.dig "classes", "size"
-
19
density = css.dig "classes", "density"
-
-
19
"screen screen--#{name} screen--#{bit_depth}bit screen--#{orientation} " \
-
"#{size} screen--1x #{density}".strip.squeeze " "
-
end
-
-
22
then: 20
else: 1
def orientation = rotation.zero? ? "landscape" : "portrait"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/core"
-
-
1
module Terminus
-
1
module Structs
-
# The palette struct.
-
1
class Palette < DB::Struct
-
1
def screen_attributes = {grays:, color_codes: colors}
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Structs
-
# The playlist struct.
-
1
class Playlist < DB::Struct
-
1
def automatic? = mode == "automatic"
-
-
11
then: 7
else: 3
def current_item_position(default: 1) = current_item ? current_item.position : default
-
-
1
def manual? = mode == "manual"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Structs
-
# The playlist item struct.
-
1
class PlaylistItem < DB::Struct
-
1
def cloneable_attributes
-
{
-
9
screen_id:,
-
position:,
-
repeat_interval:,
-
repeat_type:,
-
repeat_days:,
-
last_day_of_month:,
-
start_at:,
-
stop_at:,
-
hidden_at:
-
}
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/hash"
-
-
1
module Terminus
-
1
module Structs
-
# The screen struct.
-
# :reek:TooManyMethods
-
1
class Screen < DB::Struct
-
1
include Terminus::Uploaders::Image::Attachment[:image]
-
-
1
using Refinements::Hash
-
-
1
attr_reader :image_data
-
-
1
def initialize(*, store: Hanami.app[:shrine].storages[:store])
-
851
super(*)
-
851
@store = store
-
851
@attacher = image_attacher
-
end
-
-
1
def bit_depth = image_attributes.dig :metadata, :bit_depth
-
-
1
def height = image_attributes.dig :metadata, :height
-
-
1
def image_attributes = attributes[:image_data].deep_symbolize_keys
-
-
1
def image_destroy
-
19
then: 11
else: 8
store.delete image_id if image_id
-
19
attributes[:image_data].clear
-
end
-
-
1
def image_id = image_attributes[:id]
-
-
1
def image_name = image_attributes.dig :metadata, :filename
-
-
1
def image_name_with_checksum
-
5
path = Pathname(image_attributes.dig(:metadata, :filename))
-
5
extension = path.extname
-
5
path.sub_ext("-#{image_attributes.dig :metadata, :checksum}#{extension}").to_s
-
end
-
-
1
def image_open(**)
-
2
io = store.open(image_id, **)
-
2
then: 1
else: 1
yield io if block_given?
-
ensure
-
2
io.close
-
end
-
-
1
def image_size = image_attributes.dig :metadata, :size
-
-
87
then: 66
else: 20
def image_uri(**) = (store.url(image_id, **) if image_id)
-
-
1
def attach(io, **)
-
18
attacher.assign(io, **).tap { |file| attributes[:image_data] = file.data }
-
9
self
-
end
-
-
1
def mime_type = image_attributes.dig :metadata, :mime_type
-
-
1
def replace(io, **)
-
11
image_destroy
-
11
upload(io, **)
-
11
self
-
end
-
-
1
def upload(io, **)
-
202
attacher.upload(io, **).tap { |file| attributes[:image_data] = file.data }
-
101
self
-
end
-
-
1
def errors = attacher.errors
-
-
1
def valid? = errors.empty?
-
-
1
def width = image_attributes.dig :metadata, :width
-
-
1
def popover_attributes = {id:, label:, uri: image_uri, width:, height:}
-
-
1
private
-
-
1
attr_reader :store, :attacher
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Uploaders
-
# Processes binary uploads.
-
1
class Binary < Hanami.app[:shrine]
-
1
Attacher.validate do
-
8
validate_mime_type ["application/octet-stream"]
-
8
validate_extension ["bin"]
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Uploaders
-
# Processes image uploads.
-
1
class Image < Hanami.app[:shrine]
-
1
add_metadata :bit_depth do |io|
-
117
then: 109
else: 8
MiniMagick::Image.open(io.path).data["depth"] if io.respond_to? :path
-
end
-
-
118
add_metadata(:checksum) { |io| calculate_signature io, :md5 }
-
-
1
Attacher.validate do
-
16
validate_mime_type %w[image/bmp image/png]
-
16
validate_extension %w[bmp png]
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "hanami/view"
-
-
1
module Terminus
-
# The application base view.
-
1
class View < Hanami::View
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Bulk
-
1
module Devices
-
1
module Logs
-
# The delete view.
-
1
class Delete < View
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Bulk
-
1
module Firmware
-
# The delete view.
-
1
class Delete < View
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "hanami/view"
-
-
1
module Terminus
-
1
module Views
-
# The application custom view context.
-
1
class Context < Hanami::View::Context
-
1
include Deps[:htmx, :htmx_defaults]
-
-
1
def htmx? = htmx.request? request.env, :request, "true"
-
-
1
def htmx_configuration
-
374
then: 3
else: 184
content_for(:htmx_merge).then { it ? htmx_defaults.merge(it) : htmx_defaults }
-
.to_json
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Dashboard
-
# The show view.
-
1
class Show < View
-
1
include Deps[
-
device_relation: "relations.device",
-
extension_relation: "relations.extension",
-
firmware_relation: "relations.firmware",
-
model_relation: "relations.model",
-
playlist_relation: "relations.playlist",
-
screen_relation: "relations.screen",
-
user_relation: "relations.user"
-
]
-
-
1
expose :api_uri
-
1
expose :ip_addresses
-
1
expose :firmware
-
56
expose(:device_count) { device_relation.count }
-
56
expose(:extension_count) { extension_relation.count }
-
56
expose(:firmware_count) { firmware_relation.count }
-
56
expose(:model_count) { model_relation.count }
-
56
expose(:playlist_count) { playlist_relation.count }
-
56
expose(:screen_count) { screen_relation.count }
-
56
expose(:user_count) { user_relation.count }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Designer
-
# The show view.
-
1
class Show < View
-
1
expose :name, default: :terminus_designer
-
1
expose :label, default: "Designer"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Views
-
1
module Devices
-
# The edit view.
-
1
class Edit < View
-
1
expose :models
-
1
expose :playlists
-
1
expose :device
-
1
expose :fields, default: Core::EMPTY_HASH
-
1
expose :errors, default: Core::EMPTY_HASH
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Devices
-
# The index view.
-
1
class Index < View
-
1
expose :devices
-
1
expose :query
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Devices
-
1
module Logs
-
# The index view.
-
1
class Index < View
-
1
expose :device
-
1
expose :logs
-
1
expose :query
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Devices
-
1
module Logs
-
# The show view.
-
1
class Show < View
-
1
expose :device
-
1
expose :log
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Views
-
1
module Devices
-
# The new view.
-
1
class New < View
-
1
expose :models
-
1
expose :playlists
-
1
expose :device
-
1
expose :fields, default: Core::EMPTY_HASH
-
1
expose :errors, default: Core::EMPTY_HASH
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Devices
-
# The show view.
-
1
class Show < View
-
1
expose :device
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Extensions
-
1
module Build
-
# The new view.
-
1
class New < View
-
1
expose :extension
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Extensions
-
1
module Clone
-
# The new view.
-
1
class New < Views::Extensions::New
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Extensions
-
# The dynamic view.
-
1
class Dynamic < View
-
1
config.layout = "extension"
-
-
1
expose :content
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Views
-
1
module Extensions
-
# The edit view.
-
1
class Edit < View
-
1
include Deps[
-
model_repository: "repositories.model",
-
device_repository: "repositories.device",
-
exchange_repository: "repositories.extension_exchange"
-
]
-
-
7
expose(:default_model) { model_repository.find_by name: "og_plus" }
-
13
expose(:models) { model_repository.all.map { [it.label, it.id] } }
-
7
expose(:devices) { device_repository.all.map { [it.label, it.id] } }
-
7
expose(:exchanges) { |extension:| exchange_repository.where extension_id: extension.id }
-
1
expose :extension
-
1
expose :fields, default: Core::EMPTY_HASH
-
1
expose :errors, default: Core::EMPTY_HASH
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Views
-
1
module Extensions
-
1
module Exchanges
-
# The edit view.
-
1
class Edit < View
-
1
expose :extension
-
1
expose :exchange
-
1
expose :fields, default: Core::EMPTY_HASH
-
1
expose :errors, default: Core::EMPTY_HASH
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Extensions
-
1
module Exchanges
-
# The index view.
-
1
class Index < View
-
1
expose :extension
-
1
expose :exchanges
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Views
-
1
module Extensions
-
1
module Exchanges
-
# The new view.
-
1
class New < View
-
1
expose :extension
-
1
expose :exchange
-
1
expose :fields, default: Core::EMPTY_HASH
-
1
expose :errors, default: Core::EMPTY_HASH
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Extensions
-
1
module Gallery
-
# The index view.
-
1
class Index < Hanami::View
-
1
expose :recipe
-
1
expose :query, decorate: false
-
1
expose :page, decorate: false
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Extensions
-
# The index view.
-
1
class Index < Hanami::View
-
1
expose :extensions
-
1
expose :query
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Views
-
1
module Extensions
-
# The new view.
-
1
class New < View
-
1
include Deps[
-
model_repository: "repositories.model",
-
device_repository: "repositories.device"
-
]
-
-
11
expose(:models) { model_repository.all.map { [it.label, it.id] } }
-
8
expose(:devices) { device_repository.all.map { [it.label, it.id] } }
-
1
expose :extension
-
1
expose :fields, default: Core::EMPTY_HASH
-
1
expose :errors, default: Core::EMPTY_HASH
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Extensions
-
1
module Sensors
-
# The show view.
-
1
class Index < View
-
1
expose :content
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Extensions
-
1
module Sources
-
# The index view.
-
1
class Index < View
-
1
expose :content
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Views
-
1
module Firmware
-
# The edit view.
-
1
class Edit < View
-
1
expose :firmware
-
1
expose :fields, default: Core::EMPTY_HASH
-
1
expose :errors, default: Core::EMPTY_HASH
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Firmware
-
# The index view.
-
1
class Index < View
-
1
expose :firmware
-
1
expose :query
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Views
-
1
module Firmware
-
# The new view.
-
1
class New < View
-
1
expose :firmware
-
1
expose :fields, default: Core::EMPTY_HASH
-
1
expose :errors, default: Core::EMPTY_HASH
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Firmware
-
# The show view.
-
1
class Show < View
-
1
expose :firmware
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "htmx"
-
1
require "refinements/hash"
-
1
require "refinements/string"
-
-
1
module Terminus
-
1
module Views
-
# The view helpers.
-
1
module Helpers
-
1
extend Hanami::View::Helpers::TagHelper
-
-
1
using Refinements::Hash
-
1
using Refinements::String
-
-
1
module_function
-
-
1
def boolean value
-
6
then: 5
else: 1
css_class = value == true ? "bit-text-green" : "bit-text-red"
-
6
tag.span value.to_s, class: css_class
-
end
-
-
# rubocop:todo Metrics/ParameterLists
-
1
def field_included? key, value, attributes, record = nil
-
94
((record && record.public_send(key)) || attributes[key]).include? value
-
end
-
# rubocop:enable Metrics/ParameterLists
-
-
1
def field_for key, attributes, record = nil
-
538
else: 326
then: 212
return attributes[key] unless record
-
-
326
value = attributes.fetch_value key, record.public_send(key)
-
-
326
when: 1
case value
-
1
when: 7
when Sequel::SQLTime then value.strftime("%H:%M:%S")
-
7
else: 318
when Time then value.strftime("%Y-%m-%dT%H:%M")
-
318
else value
-
end
-
end
-
-
1
def git_link kernel: Kernel
-
269
settings = Hanami.app[:settings]
-
269
tag_sha = kernel.`("git rev-parse --quiet --short #{settings.git_tag}^{}").strip
-
-
269
then: 1
else: 268
tag_sha == settings.git_latest_sha ? git_version_link : git_latest_link
-
end
-
-
1
def git_latest_link
-
269
settings = Hanami.app[:settings]
-
-
269
link_to "Latest (ahead of #{settings.git_tag})",
-
"https://github.com/usetrmnl/terminus/commit/#{settings.git_latest_sha}",
-
class: :link
-
end
-
-
1
def git_version_link
-
2
tag = Hanami.app[:settings].git_tag
-
-
2
link_to "Version #{tag}",
-
"https://github.com/usetrmnl/terminus/releases/tag/#{tag}",
-
class: :link
-
end
-
-
59
then: 53
else: 5
def human_at(value) = (value.strftime "%B %d %Y at %H:%M %Z" if value)
-
-
11
then: 1
else: 9
def human_time(value) = (value.strftime "%I:%M %p" if value)
-
-
1
def pluralize value, suffix, count = 0
-
62
%(#{count} #{value.pluralize suffix, count})
-
end
-
-
1
def select_options_for records, label: :label, id: :id
-
37
records.reduce [["Select...", Core::EMPTY_STRING]] do |options, record|
-
36
options.append [record.public_send(label), record.public_send(id)]
-
end
-
end
-
-
1
def size value, kilobyte: 1_024, units: %w[B KB MB GB TB]
-
26
bytes = value.to_f
-
26
index = 0
-
-
26
body: 11
while bytes >= kilobyte && index < units.length - 1
-
11
bytes /= kilobyte
-
11
index += 1
-
end
-
-
26
"#{bytes.round 2} #{units[index]}"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Models
-
1
module Clone
-
# The new view.
-
1
class New < Models::New
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Views
-
1
module Models
-
# The edit view.
-
1
class Edit < View
-
1
include Deps["aspects.models.palette_optioner"]
-
-
1
expose :model
-
5
expose(:palette_options) { |model:| palette_optioner.call model }
-
1
expose :fields, default: Core::EMPTY_HASH
-
1
expose :errors, default: Core::EMPTY_HASH
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Models
-
# The index view.
-
1
class Index < Hanami::View
-
1
expose :models
-
1
expose :query
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Views
-
1
module Models
-
# The new view.
-
1
class New < View
-
1
include Deps["aspects.models.palette_optioner"]
-
-
1
expose :model
-
8
expose(:palette_options) { |model: nil| palette_optioner.call model }
-
1
expose :fields, default: Core::EMPTY_HASH
-
1
expose :errors, default: Core::EMPTY_HASH
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Models
-
# The show view.
-
1
class Show < View
-
1
expose :model
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "hanami/view"
-
1
require "refinements/struct"
-
-
1
module Terminus
-
1
module Views
-
1
module Parts
-
# The device presenter.
-
1
class Device < Hanami::View::Part
-
1
include Deps["aspects.screens.fetcher", "aspects.screens.placeholder"]
-
-
1
using Refinements::Struct
-
-
1
def battery_percentage
-
48
then: 1
else: 47
battery_charge.positive? ? battery_charge : battery_voltage_to_percent
-
end
-
-
7
then: 5
else: 1
def wake_description = String(wake_reason).empty? ? "Unknown." : wake_reason
-
-
1
def wifi_percentage
-
46
when: 9
case wifi
-
9
when: 2
when 0 then 0
-
2
when: 1
when ..-91 then 10
-
1
when: 1
when -90..-81 then 20
-
1
when: 1
when -80..-71 then 30
-
1
when: 1
when -70..-67 then 40
-
1
when: 1
when -66..-62 then 50
-
1
when: 1
when -61..-57 then 60
-
1
when: 1
when -56..-52 then 70
-
1
when: 27
when -51..-47 then 80
-
27
else: 1
when -46..-40 then 90
-
1
else 100
-
end
-
end
-
-
1
def dimensions = "#{width}x#{height}"
-
-
1
def current_screen
-
24
fetcher.call(value).either -> screen { screen },
-
14
proc { placeholder.with id: id }
-
end
-
-
1
private
-
-
1
def battery_voltage_to_percent
-
47
when: 9
case battery_voltage
-
9
when: 2
when 0 then 0
-
2
when: 1
when ..0.45 then 10
-
1
when: 1
when 0.46..0.9 then 20
-
1
when: 1
when 1.0..1.35 then 30
-
1
when: 1
when 1.36..1.8 then 40
-
1
when: 1
when 1.81..2.25 then 50
-
1
when: 27
when 2.26..2.7 then 60
-
27
when: 1
when 2.71..3.15 then 70
-
1
when: 1
when 3.16..3.6 then 80
-
1
else: 2
when 3.61..4.05 then 90
-
2
else 100
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "hanami/view"
-
1
require "initable"
-
1
require "refinements/string"
-
-
1
module Terminus
-
1
module Views
-
1
module Parts
-
# The extension exchange presenter.
-
1
class Exchange < Hanami::View::Part
-
1
include Deps["aspects.extensions.curler", "aspects.extensions.uri_builder"]
-
1
include Initable[json_formatter: Aspects::JSONFormatter]
-
-
1
using Refinements::String
-
-
1
def curl(extension) = curler.call extension, value
-
-
1
def formatted_body = json_formatter.call body
-
-
1
def formatted_data = json_formatter.call data
-
-
1
def formatted_errors = json_formatter.call errors
-
-
1
def formatted_headers = json_formatter.call headers
-
-
1
def formatted_verb = verb.upcase
-
-
1
def requests extension, length = 50
-
10
uri_builder.call(extension, template).map { it.trim_end length }
-
end
-
-
1
def status
-
9
span = helpers.tag.method :span
-
-
9
then: 7
if errors.empty?
-
7
span.call "Success", class: "bit-pill bit-pill-active"
-
else: 2
else
-
2
span.call "Failure", class: "bit-pill bit-pill-alert"
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/core"
-
1
require "hanami/view"
-
1
require "initable"
-
-
1
module Terminus
-
1
module Views
-
1
module Parts
-
# The extension presenter.
-
1
class Extension < Hanami::View::Part
-
1
include Initable[json_formatter: Aspects::JSONFormatter]
-
-
1
def alpine_tags
-
15
Array(tags).map { %('#{it}') }
-
.join(",")
-
12
.then { "[#{it}]" }
-
end
-
-
1
def formatted_data = json_formatter.call data
-
-
4
then: 2
else: 1
def formatted_days = days ? days.join(",") : ""
-
-
1
def formatted_fields = json_formatter.call fields
-
-
1
def formatted_start_at
-
2
then: 1
else: 1
start_at ? start_at.strftime("%Y-%m-%dT%H:%M:%S") : "2025-01-01T00:00:00"
-
end
-
-
1
def formatted_static_body = json_formatter.call static_body
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "hanami/view"
-
-
1
module Terminus
-
1
module Views
-
1
module Parts
-
# The firmware presenter.
-
1
class Firmware < Hanami::View::Part
-
1
def kind_label
-
11
when: 1
case kind
-
1
else: 10
when "trmnl" then kind.upcase
-
10
else kind.capitalize
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "hanami/view"
-
-
1
module Terminus
-
1
module Views
-
1
module Parts
-
# The dashboard presenter.
-
1
class IPAddress < Hanami::View::Part
-
1
def address = addr.ip_address
-
-
1
def address_with_kind = "#{address} (#{kind})"
-
-
59
then: 2
else: 56
def kind = name == "en0" ? :wireless : :wired
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "hanami/view"
-
1
require "initable"
-
1
require "refinements/array"
-
-
1
module Terminus
-
1
module Views
-
1
module Parts
-
# The model presenter.
-
1
class Model < Hanami::View::Part
-
1
include Deps[
-
join_repository: "repositories.model_palette",
-
palette_repository: "repositories.palette"
-
]
-
1
include Initable[json_formatter: Aspects::JSONFormatter]
-
-
1
using Refinements::Array
-
-
1
def allowed_palettes
-
5
join_repository.where(model_id: id)
-
.map(&:palette_id)
-
5
.then { |ids| palette_repository.where(id: ids) }
-
.map(&:label)
-
5
then: 4
else: 1
.then { it.empty? ? ["All"] : it }
-
.to_sentence
-
end
-
-
6
then: 1
else: 4
def default_palette_label = default_palette_id ? default_palette.label : "None"
-
-
1
def dimensions = "#{width}x#{height}"
-
-
1
def formatted_css = json_formatter.call css
-
-
1
def kind_label
-
17
when: 2
case kind
-
2
else: 15
when "byod", "trmnl" then kind.upcase
-
15
else kind.capitalize
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "hanami/view"
-
-
1
module Terminus
-
1
module Views
-
1
module Parts
-
# The playlist presenter.
-
1
class Playlist < Hanami::View::Part
-
1
include Deps["aspects.screens.placeholder"]
-
-
1
def current_screen_pill item, label = "Current Screen"
-
3
else: 2
then: 1
return unless current_item_id == item.id
-
-
2
helpers.tag.div label, class: "bit-pill bit-pill-active"
-
end
-
-
# :reek:ManualDispatch
-
1
def current_screen
-
15
then: 3
if current_item_id && respond_to?(:current_item)
-
3
current_item.screen
-
else: 12
else
-
12
placeholder.with id:, uri: "blank.svg"
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "hanami/view"
-
-
1
module Terminus
-
1
module Views
-
1
module Parts
-
# The screen presenter.
-
1
class Screen < Hanami::View::Part
-
14
then: 9
else: 4
def dimensions = width && height ? "#{width}x#{height}" : "Unknown"
-
-
3
then: 1
else: 1
def type = mime_type ? mime_type.delete_prefix("image/").upcase : "Unknown"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "hanami/view"
-
-
1
module Terminus
-
1
module Views
-
1
module Parts
-
# The user presenter.
-
1
class User < Hanami::View::Part
-
1
attr_accessor :password
-
-
1
def pill
-
14
when: 5
case status_id
-
5
when: 7
when 1 then "caution"
-
7
when: 1
when 2 then "active"
-
1
else: 1
when 3 then "inactive"
-
1
else "unknown"
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Playlists
-
1
module Clone
-
# The new view.
-
1
class New < Views::Playlists::New
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Views
-
1
module Playlists
-
# The edit view.
-
1
class Edit < View
-
1
expose :playlist
-
1
expose :items, default: Core::EMPTY_ARRAY
-
1
expose :fields, default: Core::EMPTY_HASH
-
1
expose :errors, default: Core::EMPTY_HASH
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Playlists
-
# The index view.
-
1
class Index < View
-
1
expose :playlists
-
1
expose :query
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Playlists
-
1
module Items
-
# The index view.
-
1
class Index < View
-
1
expose :playlist_id
-
1
expose :items
-
1
expose :query
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Views
-
1
module Playlists
-
1
module Items
-
# The new view.
-
1
class New < View
-
1
expose :playlist
-
1
expose :screen_options, decorate: false
-
1
expose :item
-
1
expose :fields, default: Core::EMPTY_HASH
-
1
expose :errors, default: Core::EMPTY_HASH
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Playlists
-
1
module Items
-
# The show view.
-
1
class Show < View
-
1
expose :item
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Playlists
-
1
module Mirror
-
# The edit view.
-
1
class Edit < View
-
1
expose :playlist
-
1
expose :devices
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Views
-
1
module Playlists
-
# The new view.
-
1
class New < View
-
1
expose :playlist
-
1
expose :fields, default: Core::EMPTY_HASH
-
1
expose :errors, default: Core::EMPTY_HASH
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Playlists
-
1
module Screens
-
# The show view.
-
1
class Show < View
-
1
include Deps[:routes]
-
-
1
expose :playlist
-
1
expose :current
-
12
expose(:index, decorate: false) { |screens, current:| screens.index current }
-
12
expose(:total, decorate: false) { |screens| screens.size - 1 }
-
-
1
expose :status, decorate: false do |index, total|
-
11
then: 10
else: 1
"#{index + 1} of #{total + 1}" if index && total
-
end
-
-
1
expose :previous_uri, decorate: false do |playlist:, before:|
-
11
then: 10
else: 1
routes.path :playlist_screen, playlist_id: playlist.id, id: before.id if before
-
end
-
-
1
expose :next_uri, decorate: false do |playlist:, after:|
-
11
then: 10
else: 1
routes.path :playlist_screen, playlist_id: playlist.id, id: after.id if after
-
end
-
-
1
expose :first_uri, decorate: false do |screens, playlist:|
-
11
first = screens.first
-
11
then: 10
else: 1
routes.path :playlist_screen, playlist_id: playlist.id, id: first.id if first
-
end
-
-
1
expose :last_uri, decorate: false do |screens, playlist:|
-
11
last = screens.last
-
11
then: 10
else: 1
routes.path :playlist_screen, playlist_id: playlist.id, id: last.id if last
-
end
-
-
1
private_expose :routes
-
1
private_expose :before, decorate: false
-
1
private_expose :after, decorate: false
-
12
private_expose(:screens, decorate: false) { |playlist:| playlist.screens }
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Playlists
-
# The show view.
-
1
class Show < View
-
1
expose :playlist
-
1
expose :items
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module ProblemDetails
-
# The index view.
-
1
class Index < View
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "refinements/array"
-
-
1
module Terminus
-
1
module Views
-
1
module Scopes
-
# Groups form label and input together as a single form field.
-
1
class FormField < Hanami::View::Scope
-
1
using Refinements::Array
-
-
1
def alpine
-
619
else: 126
then: 493
return unless locals.key? :alpine
-
-
253
locals[:alpine].transform_keys! { "x-#{it}" }
-
127
.map { |key, value| %(#{key}="#{value}") }
-
.join(" ")
-
126
.then { %( #{it}) }
-
end
-
-
1
def toggle_error kind = "form-field"
-
621
then: 31
else: 590
errors.fetch(key, Core::EMPTY_ARRAY).any? ? [kind, "error"].compact.join(" ") : kind
-
end
-
-
1
def error_message
-
649
else: 648
then: 1
return Core::EMPTY_STRING unless locals.key? :errors
-
648
else: 59
then: 589
return Core::EMPTY_STRING unless errors.key? key
-
-
59
errors[key].to_sentence
-
end
-
-
1
def render(path = "shared/form_field") = super
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Scopes
-
# Provides customized popover content.
-
1
class PopoverDefaultContent < Hanami::View::Scope
-
1
def dom_id = "popover-#{name}"
-
-
1
def render(path = "shared/popovers/content/default") = super
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Scopes
-
# Provides customized popover content.
-
1
class PopoverScreenContent < Hanami::View::Scope
-
1
def dom_id = "popover-screen-#{id}"
-
-
1
def width = locals.fetch __method__, 800
-
-
1
def height = locals.fetch __method__, 480
-
-
1
def render(path = "shared/popovers/content/screen") = super
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Views
-
1
module Screens
-
# The edit view.
-
1
class Edit < View
-
1
expose :models
-
1
expose :screen
-
1
expose :fields, default: Core::EMPTY_HASH
-
1
expose :errors, default: Core::EMPTY_HASH
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Screens
-
1
module Gaffe
-
# The new view.
-
1
class New < View
-
1
config.layout = "gaffe"
-
-
1
expose :message, decorate: false
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Screens
-
# The index view.
-
1
class Index < Hanami::View
-
1
expose :screens
-
1
expose :query
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Views
-
1
module Screens
-
# The new view.
-
1
class New < View
-
1
expose :models
-
1
expose :screen
-
1
expose :fields, default: Core::EMPTY_HASH
-
1
expose :errors, default: Core::EMPTY_HASH
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Screens
-
# The show view.
-
1
class Show < View
-
1
expose :screen
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Screens
-
1
module Sleep
-
# The new view.
-
1
class New < View
-
1
config.layout = "sleep"
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Screens
-
1
module Welcome
-
# The new view.
-
1
class New < View
-
1
config.layout = "welcome"
-
-
1
expose :device
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Views
-
1
module Users
-
# The edit view.
-
1
class Edit < View
-
1
expose :user
-
1
expose :statuses
-
1
expose :fields, default: Core::EMPTY_HASH
-
1
expose :errors, default: Core::EMPTY_HASH
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Users
-
# The index view.
-
1
class Index < Hanami::View
-
1
expose :users
-
1
expose :query
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Terminus
-
1
module Views
-
1
module Users
-
# The new view.
-
1
class New < View
-
1
expose :user
-
1
expose :statuses
-
1
expose :fields, default: Core::EMPTY_HASH
-
1
expose :errors, default: Core::EMPTY_HASH
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Views
-
1
module Users
-
# The show view.
-
1
class Show < View
-
1
expose :user
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
Hanami.app.register_provider :htmx do
-
2
prepare { require "htmx" }
-
-
1
start do
-
1
toggler = lambda do |request, default = "app"|
-
106
then: 67
else: 39
HTMX.request?(request.env, :request, "true") ? false : default
-
end
-
-
1
register :htmx, HTMX
-
1
register :htmx_defaults, {"allowScriptTags" => false, "defaultSwapStyle" => "outerHTML"}.freeze
-
1
register :htmx_layout, toggler
-
end
-
end
-
# frozen_string_literal: true
-
-
1
Hanami.app.register_provider :http do
-
1
prepare do
-
1
require "connection_pool"
-
1
require "http"
-
end
-
-
1
start do
-
1
slice.start :logger
-
-
1
connect, read, write = slice[:settings].to_h.values_at :http_timeout_connect,
-
:http_timeout_read,
-
:http_timeout_write
-
-
1
http = ConnectionPool::Wrapper.new size: ENV.fetch("HANAMI_MAX_THREADS", 5) do
-
1
HTTP.timeout(connect:, read:, write:)
-
.use(:auto_inflate)
-
.use(logging: {logger: slice[:logger]})
-
.headers("User-Agent" => "http.rb/#{HTTP::VERSION} (#{Hanami.app.app_name})")
-
end
-
-
1
register :http, http
-
end
-
-
1
stop { slice[:http].close }
-
end
-
# frozen_string_literal: true
-
-
1
Hanami.app.register_provider :liquid, namespace: true do
-
2
prepare { require "trmnl/liquid" }
-
-
1
start do
-
2
default = TRMNL::Liquid.new { |environment| environment.error_mode = :strict }
-
-
1
basic = lambda do |template, data, environment: default|
-
22
Liquid::Template.parse(template, environment:).render(data)
-
end
-
-
1
sanitize = lambda do |template, data, environment: default|
-
19
slice["aspects.sanitizer"].call Liquid::Template.parse(template, environment:).render(data)
-
end
-
-
1
register :basic, basic
-
1
register :sanitize, sanitize
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../../app/providers/logger"
-
-
1
Hanami.app.register_provider :logger, source: Terminus::Providers::Logger
-
# frozen_string_literal: true
-
-
1
require "refinements/pathname"
-
-
1
using Refinements::Pathname
-
-
1
Hanami.app.register_provider :mini_magick, namespace: true do
-
2
prepare { require "mini_magick" }
-
-
1
start do
-
1
MiniMagick.configure do |config|
-
1
config.errors = true
-
1
config.warnings = true
-
1
config.restricted_env = true
-
1
config.tmpdir = slice.root.join("tmp/mini_magick").make_ancestors.make_dir
-
1
config.logger = slice[:logger]
-
end
-
-
1
register :core, MiniMagick
-
1
register :image, MiniMagick::Image
-
end
-
end
-
# frozen_string_literal: true
-
-
1
Hanami.app.register_provider :shrine do
-
1
prepare do
-
1
require "shrine"
-
1
require "shrine/storage/file_system"
-
end
-
-
1
start do
-
1
then: 1
Shrine.storages = if Hanami.env? :test
-
1
{cache: Shrine::Storage::Memory.new, store: Shrine::Storage::Memory.new}
-
else
-
{
-
skipped
# :nocov:
-
skipped
cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"),
-
skipped
store: Shrine::Storage::FileSystem.new("public", prefix: "uploads")
-
skipped
# :nocov:
-
}
-
end
-
-
1
Shrine.plugin :add_metadata
-
1
Shrine.plugin :determine_mime_type, analyzer: :marcel
-
1
Shrine.plugin :entity
-
1
Shrine.plugin :signature
-
48
Shrine.plugin :store_dimensions, analyzer: :mini_magick, on_error: proc { "Omit" }
-
1
Shrine.plugin :validation_helpers
-
-
1
Shrine.logger = slice[:logger]
-
-
1
register :shrine, Shrine
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../../app/providers/logger"
-
-
1
Hanami.app.register_provider :sidekiq, source: Terminus::Providers::Sidekiq
-
# frozen_string_literal: true
-
-
1
Hanami.app.register_provider :trmnl_api do
-
2
prepare { require "trmnl/api" }
-
-
1
start do
-
1
slice.start :http
-
-
1
TRMNL::API::Container.register :http, slice[:http]
-
1
TRMNL::API::Container.register :logger, slice[:logger]
-
-
2
recipes = TRMNL::API.new { |settings| settings.uri = "https://trmnl.com" }
-
-
1
register :trmnl_api, TRMNL::API.new
-
1
register :trmnl_api_recipes, recipes
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sidekiq/web"
-
-
1
require "sidekiq-scheduler/web"
-
-
1
require_relative "../app/aspects/screens/designer/middleware"
-
-
1
module Terminus
-
# The application base routes.
-
# rubocop:todo Metrics/ClassLength
-
1
class Routes < Hanami::Routes
-
2
slice(:authentication, at: "/") { use Authentication::Middleware }
-
-
1
mount Sidekiq::Web, at: "/sidekiq"
-
-
1
get "/", to: "dashboard.show", as: :root
-
-
# rubocop:todo Metrics/BlockLength
-
1
scope "api" do
-
1
get "/devices", to: "api.devices.index", as: :devices
-
1
get "/devices/:id", to: "api.devices.show", as: :device
-
1
post "/devices", to: "api.devices.create", as: :device_create
-
1
patch "/devices/:id", to: "api.devices.patch", as: :device_patch
-
1
delete "/devices/:id", to: "api.devices.delete", as: :device_delete
-
-
1
resource :display, to: "api.display", only: :show
-
-
1
get "/firmware", to: "api.firmware.index", as: :firmware
-
1
get "/firmware/:id", to: "api.firmware.show", as: :firmware_show
-
1
post "/firmware", to: "api.firmware.create", as: :firmware_create
-
1
patch "/firmware/:id", to: "api.firmware.patch", as: :firmware_patch
-
1
delete "/firmware/:id", to: "api.firmware.delete", as: :firmware_delete
-
-
1
resource :log, to: "api.log", only: :create
-
-
1
get "/models", to: "api.models.index", as: :models
-
1
get "/models/:id", to: "api.models.show", as: :model
-
1
post "/models", to: "api.models.create", as: :model_create
-
1
patch "/models/:id", to: "api.models.patch", as: :model_patch
-
1
delete "/models/:id", to: "api.models.delete", as: :model_delete
-
-
1
get "/playlists", to: "api.playlists.index", as: :playlists
-
1
get "/playlists/:id", to: "api.playlists.show", as: :playlist
-
1
post "/playlists", to: "api.playlists.create", as: :playlist_create
-
1
patch "/playlists/:id", to: "api.playlists.patch", as: :playlist_patch
-
1
delete "/playlists/:id", to: "api.playlists.delete", as: :playlist_delete
-
-
1
get "/screens", to: "api.screens.index", as: :screens
-
1
post "/screens", to: "api.screens.create", as: :screen_create
-
1
patch "/screens/:id", to: "api.screens.patch", as: :screen_patch
-
1
delete "/screens/:id", to: "api.screens.delete", as: :screen_delete
-
-
1
resource :setup, to: "api.setup", only: :show
-
end
-
# rubocop:enable Metrics/BlockLength
-
-
1
scope "bulk" do
-
1
delete "/devices/:device_id/logs", to: "bulk.devices.logs.delete", as: :device_logs_delete
-
1
delete "/firmware", to: "bulk.firmware.delete", as: :firmware_delete
-
end
-
-
1
get "/devices", to: "devices.index", as: :devices
-
1
get "/devices/:id", to: "devices.show", as: :device
-
1
get "/devices/new", to: "devices.new", as: :device_new
-
1
post "/devices", to: "devices.create", as: :device_create
-
1
get "/devices/:id/edit", to: "devices.edit", as: :device_edit
-
1
put "/devices/:id", to: "devices.update", as: :device_update
-
1
delete "/devices/:id", to: "devices.delete", as: :device_delete
-
-
1
get "/devices/:device_id/logs", to: "devices.logs.index", as: :device_logs
-
1
get "/devices/:device_id/logs/:id", to: "devices.logs.show", as: :device_log
-
1
delete "/devices/:device_id/logs/:id", to: "devices.logs.delete", as: :device_log_delete
-
-
1
resource :designer, to: "designer", only: %i[show create]
-
-
1
get "/extensions", to: "extensions.index", as: :extensions
-
1
get "/extensions/new", to: "extensions.new", as: :extension_new
-
1
post "/extensions", to: "extensions.create", as: :extension_create
-
1
get "/extensions/:id/edit", to: "extensions.edit", as: :extension_edit
-
1
put "/extensions/:id", to: "extensions.update", as: :extension_update
-
1
delete "/extensions/:id", to: "extensions.delete", as: :extension_delete
-
-
1
get "/extensions/gallery", to: "extensions.gallery.index", as: :extensions_gallery
-
1
post "/extensions/gallery", to: "extensions.gallery.create", as: :extensions_gallery_create
-
-
1
post "/extensions/:extension_id/build",
-
to: "extensions.build.create",
-
as: :extension_build_create
-
-
1
get "/extensions/:extension_id/clone/new", to: "extensions.clone.new", as: :extension_clone_new
-
1
post "/extensions/:extension_id/clone",
-
to: "extensions.clone.create",
-
as: :extension_clone_create
-
-
1
get "/extensions/:extension_id/exchanges",
-
to: "extensions.exchanges.index",
-
as: :extension_exchanges
-
1
get "/extensions/:extension_id/exchanges/new",
-
to: "extensions.exchanges.new",
-
as: :extension_exchange_new
-
1
post "/extensions/:extension_id/exchanges",
-
to: "extensions.exchanges.create",
-
as: :extension_exchanges
-
1
get "/extensions/:extension_id/exchanges/:id/edit",
-
to: "extensions.exchanges.edit",
-
as: :extension_exchange_edit
-
1
put "/extensions/:extension_id/exchanges/:id",
-
to: "extensions.exchanges.update",
-
as: :extension_exchange
-
1
delete "/extensions/:extension_id/exchanges/:id",
-
to: "extensions.exchanges.delete",
-
as: :extension_exchange
-
-
1
get "/extensions/:extension_id/export", to: "extensions.export.show", as: :extension_export
-
1
get "/extensions/:extension_id/preview", to: "extensions.preview.show", as: :extension_preview
-
1
get "/extensions/:extension_id/sources", to: "extensions.sources.index", as: :extension_sources
-
1
get "/extensions/:extension_id/sensors", to: "extensions.sensors.index", as: :extension_sensors
-
-
1
get "/firmware", to: "firmware.index", as: :firmware
-
1
get "/firmware/:id", to: "firmware.show", as: :firmware_show
-
1
get "/firmware/new", to: "firmware.new", as: :firmware_new
-
1
post "/firmware", to: "firmware.create", as: :firmware_create
-
1
get "/firmware/:id/edit", to: "firmware.edit", as: :firmware_edit
-
1
put "/firmware/:id", to: "firmware.update", as: :firmware_update
-
1
delete "/firmware/:id", to: "firmware.delete", as: :firmware_delete
-
-
1
get "/models", to: "models.index", as: :models
-
1
get "/models/:id", to: "models.show", as: :model
-
1
get "/models/new", to: "models.new", as: :model_new
-
1
post "/models", to: "models.create", as: :model_create
-
1
get "/models/:id/edit", to: "models.edit", as: :model_edit
-
1
put "/models/:id", to: "models.update", as: :model_update
-
1
delete "/models/:id", to: "models.delete", as: :model_delete
-
-
1
get "/models/:model_id/clone/new", to: "models.clone.new", as: :model_clone_new
-
1
post "/models/:model_id/clone", to: "models.clone.create", as: :model_clone_create
-
-
1
get "/playlists", to: "playlists.index", as: :playlists
-
1
get "/playlists/:id", to: "playlists.show", as: :playlist
-
1
get "/playlists/new", to: "playlists.new", as: :playlist_new
-
1
post "/playlists", to: "playlists.create", as: :playlist_create
-
1
get "/playlists/:id/edit", to: "playlists.edit", as: :playlist_edit
-
1
put "/playlists/:id", to: "playlists.update", as: :playlist_update
-
1
delete "/playlists/:id", to: "playlists.delete", as: :playlist_delete
-
-
1
get "/playlists/:playlist_id/clone/new", to: "playlists.clone.new", as: :playlist_clone_new
-
1
post "/playlists/:playlist_id/clone", to: "playlists.clone.create", as: :playlist_clone_create
-
-
1
get "/playlists/:playlist_id/items", to: "playlists.items.index", as: :playlist_items
-
1
get "/playlists/:playlist_id/items/:id", to: "playlists.items.show", as: :playlist_item
-
1
get "/playlists/:playlist_id/items/new", to: "playlists.items.new", as: :playlist_item_new
-
1
post "/playlists/:playlist_id/items", to: "playlists.items.create", as: :playlist_item_create
-
1
get "/playlists/:playlist_id/items/:id/edit",
-
to: "playlists.items.edit",
-
as: :playlist_item_edit
-
1
put "/playlists/:playlist_id/items/:id", to: "playlists.items.update", as: :playlist_item_update
-
1
delete "/playlists/:playlist_id/items/:id",
-
to: "playlists.items.delete",
-
as: :playlist_item_delete
-
-
1
get "/playlists/:playlist_id/mirror/edit",
-
to: "playlists.mirror.edit",
-
as: :playlist_mirror_edit
-
1
put "/playlists/:playlist_id/mirror", to: "playlists.mirror.update", as: :playlist_mirror_update
-
-
1
get "/playlists/:playlist_id/screens", to: "playlists.screens.index", as: :playlist_screens
-
1
get "/playlists/:playlist_id/screens/:id", to: "playlists.screens.show", as: :playlist_screen
-
-
1
resources :problem_details, to: "problem_details", only: :index, as: :problem_details
-
-
1
get "/screens", to: "screens.index", as: :screens
-
1
get "/screens/:id", to: "screens.show", as: :screen
-
1
get "/screens/new", to: "screens.new", as: :screen_new
-
1
post "/screens", to: "screens.create", as: :screen_create
-
1
get "/screens/:id/edit", to: "screens.edit", as: :screen_edit
-
1
put "/screens/:id", to: "screens.update", as: :screen_update
-
1
delete "/screens/:id", to: "screens.delete", as: :screen_delete
-
-
1
get "/users", to: "users.index", as: :users
-
1
get "/users/:id", to: "users.show", as: :user
-
1
get "/users/new", to: "users.new", as: :user_new
-
1
post "/users", to: "users.create", as: :user_create
-
1
get "/users/:id/edit", to: "users.edit", as: :user_edit
-
1
put "/users/:id", to: "users.update", as: :user_update
-
-
2
slice(:health, at: "/up") { root to: "show" }
-
-
1
use Rack::Static, root: "public", urls: ["/.well-known/security.txt", "/fonts", "/uploads"]
-
1
use Aspects::Screens::Designer::Middleware, pattern: %r(/preview/(?<name>.+))
-
end
-
# rubocop:enable Metrics/ClassLength
-
end
-
# frozen_string_literal: true
-
-
1
require "initable"
-
1
require "socket"
-
-
1
module Terminus
-
# Finds wired and wireless addresses.
-
1
class IPFinder
-
1
include Initable[socket: Socket]
-
-
1
def all pattern: /\A(en|wl|eth)/
-
67
socket.getifaddrs.select do |interface|
-
232
address = interface.addr
-
232
interface.name.match?(pattern) && address.ipv4? && !address.ipv4_loopback?
-
end
-
end
-
-
1
def wired pattern: /\A(en[1-9]|eth)/
-
10
all.find { |address| address.name.match? pattern }
-
5
then: 3
else: 2
.then { it.addr.ip_address if it }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Terminus
-
1
module Refines
-
1
module Actions
-
# Modifies and enhances default Hanami action response behavior.
-
1
module Response
-
1
refine Hanami::Action::Response do
-
1
def with body:, format: nil, status: 200
-
50
@body = [body]
-
50
@status = status
-
-
50
then: 39
else: 11
self.format = format if format
-
50
self
-
end
-
end
-
end
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "rodauth"
-
-
1
Rodauth::Feature.define :hanami do
-
1
auth_value_method :hanami_view, nil
-
-
1
def view(name, *)
-
82
layout_path = view_base.class.layout_path view_base.config.layout
-
82
scope = view_rendering.scope rodauth: self
-
-
164
view_rendering.template(layout_path, scope) { render name }
-
end
-
-
1
def render name
-
246
else: 82
then: 164
return super unless view_template? name
-
-
82
view_rendering.template name, view_rendering.scope(rodauth: self)
-
end
-
-
1
private
-
-
1
def view_template? name
-
# rubocop:todo Style/SendWithLiteralMethodName
-
246
view_rendering.renderer.__send__ :lookup, name, view_base.config.default_format
-
# rubocop:enable Style/SendWithLiteralMethodName
-
end
-
-
1
def view_rendering
-
574
@view_rendering ||= view_base.rendering format: view_base.config.default_format,
-
context: view_context
-
end
-
-
1
def view_context
-
82
@view_context ||= begin
-
82
action_request = Hanami::Action::Request.new(
-
env: request.env,
-
params: request.params,
-
session_enabled: true
-
)
-
-
82
view_base.config.default_context.class.new request: action_request
-
end
-
end
-
-
1
def view_base
-
656
@view_base ||= hanami_view.call
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
require "roda"
-
1
require "rodauth"
-
-
1
require_relative "feature"
-
-
1
module Authentication
-
# Specialized Roda middleware for authentication.
-
1
class Middleware < Roda
-
1
UNVERIFIED_ID = 1
-
1
VERIFIED_ID = 2
-
-
1
plugin :middleware
-
-
1
plugin :rodauth, json: true do
-
1
enable :active_sessions,
-
:audit_logging,
-
:change_login,
-
:change_password,
-
:create_account,
-
:disallow_common_passwords,
-
:hanami,
-
:jwt_refresh,
-
:login,
-
:logout,
-
:remember,
-
:recovery_codes,
-
:session_expiration
-
-
1
db Authentication::Slice["db.gateway"].connection
-
-
# Feature (automatic): base
-
1
accounts_table :user
-
1
after_login { remember_login }
-
1
already_logged_in { redirect "/" }
-
1
flash_error_key :alert
-
1
hmac_secret Hanami.app[:settings].app_secret
-
1
login_label "Email"
-
1
password_hash_table :user_password_hash
-
1
require_login_error_flash "Please log in to continue."
-
1
template_opts layout: nil
-
1
unverified_account_message "Unverified user, please verify before logging in."
-
-
1
after_login do
-
114
else: 113
then: 1
unless account[:status_id] == VERIFIED_ID
-
1
logout
-
1
set_redirect_error_flash "Your account requires verification before proceeding. " \
-
"Please contact administration for access."
-
1
redirect "/login"
-
end
-
end
-
-
# Feature (automatic): login_password_requirements_base
-
1
require_password_confirmation? false
-
-
# Feature: active_sessions
-
1
active_sessions_account_id_column :user_id
-
1
active_sessions_table :user_active_session_key
-
-
# Feature: audit_logging
-
1
audit_logging_table :user_authentication_audit_log
-
1
audit_logging_account_id_column :user_id
-
-
# Feature: change_login
-
1
change_login_route "me/login"
-
3
change_login_view { view "login_update", nil }
-
-
# Feature: change_password
-
1
change_password_route "me/password"
-
2
change_password_view { view "password_update", nil }
-
1
change_password_button "Save"
-
-
# Feature: create_account
-
1
create_account_button "Create"
-
1
create_account_link_text "Register."
-
1
create_account_route "register"
-
5
create_account_view { view "register", nil }
-
1
change_login_button "Save"
-
-
1
after_create_account do
-
3
user_id = account[:id]
-
3
then: 2
else: 1
status_id = db[:user].one? ? VERIFIED_ID : UNVERIFIED_ID
-
3
account_id = db[:account].insert_conflict(target: :name, update: {name: "default"})
-
.insert name: "default", label: "Default"
-
-
3
db[:user].where(id: user_id).update(name: param("name"), status_id:)
-
3
db[:membership].insert(user_id: user_id, account_id:)
-
-
3
else: 2
then: 1
unless status_id == VERIFIED_ID
-
1
logout
-
1
set_redirect_error_flash "Your account requires verification before proceeding. " \
-
"Please contact administration for access."
-
1
redirect "/login"
-
end
-
end
-
-
# Feature (custom): hanami
-
83
hanami_view(proc { View.new })
-
-
# Feature: jwt
-
1
jwt_secret Hanami.app[:settings].app_secret
-
1
jwt_refresh_route "api/jwt"
-
-
# Feature: jwt_refresh
-
1
jwt_access_token_period Hanami.app[:settings].api_access_token_period
-
1
jwt_refresh_token_account_id_column :user_id
-
1
jwt_refresh_token_table :user_jwt_refresh_key
-
-
# Feature: login
-
1
login_error_flash "There was an error signing in."
-
1
login_form_footer_links_heading { nil }
-
1
login_notice_flash "You have been logged in."
-
1
login_return_to_requested_location? true
-
1
multi_phase_login_view { view "login_multi_phase", nil }
-
-
# Feature: logout
-
1
logout_notice_flash "You have been logged out."
-
1
logout_redirect "/"
-
-
# Feature: remember
-
1
remember_button "Save"
-
1
remember_table :user_remember_key
-
1
remember_route "me/remember"
-
-
# Feature: recovery_codes
-
1
recovery_codes_table :user_recovery_code
-
-
# Feature: session_expiration
-
1
session_inactivity_timeout Hanami.app[:settings].session_inactivity_limit
-
1
max_session_lifetime Hanami.app[:settings].session_lifetime_limit
-
end
-
-
1
route do |request|
-
508
rodauth.check_session_expiration
-
508
env["rodauth"] = rodauth
-
508
request.rodauth
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Authentication
-
# The slice view.
-
1
class View < Terminus::View
-
1
config.paths += ["app/templates"]
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Authentication
-
1
module Views
-
# The slice view context.
-
1
class Context < Hanami::View::Context
-
1
include Deps[main_assets: "main.assets"]
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Health
-
# The slice base action.
-
1
class Action < Terminus::Action
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Health
-
1
module Actions
-
# The show action.
-
1
class Show < Health::Action
-
1
handle_exception Exception => :down
-
-
1
def handle(*, response) = response.render view, color: :green
-
-
1
private
-
-
1
def down(*, response, _exception) = response.render view, color: :red, status: 503
-
end
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Health
-
# The slice base view.
-
1
class View < Terminus::View
-
end
-
end
-
# auto_register: false
-
# frozen_string_literal: true
-
-
1
module Health
-
1
module Views
-
# The slice view context.
-
1
class Context < Hanami::View::Context
-
1
include Deps[main_assets: "main.assets"]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Health
-
1
module Views
-
# The show view.
-
1
class Show < Health::View
-
1
expose :color
-
end
-
end
-
end