Compare commits
16 Commits
6209b926e2
...
a9b0fe80e4
| Author | SHA1 | Date |
|---|---|---|
|
|
a9b0fe80e4 | |
|
|
fbf93e80ae | |
|
|
95a126953b | |
|
|
30aa429c76 | |
|
|
423460df66 | |
|
|
cb19bc990d | |
|
|
a958915d41 | |
|
|
9ca6e10ac1 | |
|
|
def8d28c3b | |
|
|
cf3dae63f3 | |
|
|
f7131c827f | |
|
|
32d0ac69c9 | |
|
|
eb3bc33509 | |
|
|
cd7620672f | |
|
|
4a3d0313a2 | |
|
|
8041453c5e |
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="development.sqlite3" uuid="d3ab71a5-ebb0-4573-9e48-bc4bd013b015">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/storage/development.sqlite3</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -53,6 +53,7 @@
|
|||
<orderEntry type="library" scope="PROVIDED" name="builder (v3.3.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="bundler (v2.6.6, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="capybara (v3.40.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="ccutrer-serialport (v1.1.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.3.5, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="connection_pool (v2.5.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="crass (v1.0.6, rbenv: 3.4.2) [gem]" level="application" />
|
||||
|
|
@ -66,6 +67,7 @@
|
|||
<orderEntry type="library" scope="PROVIDED" name="erb_lint (v0.9.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="erubi (v1.13.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="et-orbi (v1.2.11, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="ffi (v1.17.2, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="foreman (v0.88.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="fugit (v1.11.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="globalid (v1.2.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||
|
|
@ -129,6 +131,7 @@
|
|||
<orderEntry type="library" scope="PROVIDED" name="rubocop-rails-omakase (v1.1.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.13.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rubyzip (v2.4.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="rufus-scheduler (v3.9.2, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="securerandom (v0.4.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="selenium-webdriver (v4.31.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="smart_properties (v1.17.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="file://$PROJECT_DIR$/app/controllers/schedules_controller.rb" dialect="GenericSQL" />
|
||||
</component>
|
||||
</project>
|
||||
1
Gemfile
1
Gemfile
|
|
@ -71,3 +71,4 @@ end
|
|||
|
||||
gem "ccutrer-serialport"
|
||||
gem "rmodbus"
|
||||
gem "rufus-scheduler"
|
||||
|
|
|
|||
22
Gemfile.lock
22
Gemfile.lock
|
|
@ -129,6 +129,8 @@ GEM
|
|||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
ffi (1.17.2-aarch64-linux-gnu)
|
||||
ffi (1.17.2-arm64-darwin)
|
||||
ffi (1.17.2-x86_64-linux-gnu)
|
||||
foreman (0.88.1)
|
||||
fugit (1.11.1)
|
||||
et-orbi (~> 1, >= 1.2.11)
|
||||
|
|
@ -192,18 +194,10 @@ GEM
|
|||
nio4r (2.7.4)
|
||||
nokogiri (1.18.7-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.7-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.7-arm-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.7-arm-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.7-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.7-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.7-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
ostruct (0.6.1)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.8.0)
|
||||
|
|
@ -303,6 +297,8 @@ GEM
|
|||
rubocop-rails (>= 2.30)
|
||||
ruby-progressbar (1.13.0)
|
||||
rubyzip (2.4.1)
|
||||
rufus-scheduler (3.9.2)
|
||||
fugit (~> 1.1, >= 1.11.1)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.31.0)
|
||||
base64 (~> 0.2)
|
||||
|
|
@ -328,12 +324,8 @@ GEM
|
|||
railties (>= 7.1)
|
||||
thor (~> 1.3.1)
|
||||
sqlite3 (2.6.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.6.0-aarch64-linux-musl)
|
||||
sqlite3 (2.6.0-arm-linux-gnu)
|
||||
sqlite3 (2.6.0-arm-linux-musl)
|
||||
sqlite3 (2.6.0-arm64-darwin)
|
||||
sqlite3 (2.6.0-x86_64-linux-gnu)
|
||||
sqlite3 (2.6.0-x86_64-linux-musl)
|
||||
sshkit (1.24.0)
|
||||
base64
|
||||
logger
|
||||
|
|
@ -377,13 +369,8 @@ GEM
|
|||
PLATFORMS
|
||||
aarch64-linux
|
||||
aarch64-linux-gnu
|
||||
aarch64-linux-musl
|
||||
arm-linux-gnu
|
||||
arm-linux-musl
|
||||
arm64-darwin-24
|
||||
x86_64-linux
|
||||
x86_64-linux-gnu
|
||||
x86_64-linux-musl
|
||||
|
||||
DEPENDENCIES
|
||||
amazing_print
|
||||
|
|
@ -403,6 +390,7 @@ DEPENDENCIES
|
|||
rails (~> 8.0.2)
|
||||
rmodbus
|
||||
rubocop-rails-omakase
|
||||
rufus-scheduler
|
||||
selenium-webdriver
|
||||
solid_cable
|
||||
solid_cache
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@
|
|||
--color-danger: rgb(239, 83, 80);
|
||||
--color-table-border: rgb(203, 213, 225);
|
||||
|
||||
/* theme */
|
||||
--color-base-background: rgb(39, 44, 56);
|
||||
--color-base-secondary: rgb(55, 61, 71);
|
||||
--color-base-text: rgb(152, 158, 172);
|
||||
--color-base-border: rgb(77, 84, 102);
|
||||
|
||||
--base-font-size: 0.85rem;
|
||||
--text-xs: calc(var(--base-font-size) * 0.75);
|
||||
--text-xs--line-height: calc(1 / var(--text-xs));
|
||||
|
|
@ -90,7 +96,11 @@
|
|||
}
|
||||
|
||||
.side-bar {
|
||||
@apply w-64 px-4 text-lg font-bold divide-y divide-default-slate-dark;
|
||||
@apply w-[16rem] px-4 text-lg font-bold divide-y divide-base-border;
|
||||
}
|
||||
|
||||
.content {
|
||||
@apply rounded-lg bg-base-secondary;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
|
|
@ -119,7 +129,7 @@
|
|||
}
|
||||
|
||||
.menu-group-icon {
|
||||
@apply py-1 text-sm text-default-slate-dark font-medium justify-center
|
||||
@apply py-1 text-sm font-medium justify-center
|
||||
}
|
||||
|
||||
.menu-group-name {
|
||||
|
|
@ -190,3 +200,23 @@
|
|||
@apply text-lg font-bold;
|
||||
}
|
||||
}
|
||||
|
||||
/* 기본 스크롤바 스타일 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(21, 21, 21, 0.4);
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(100, 100, 100, 0.6);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
class DeviceController < ApplicationController
|
||||
end
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
class ModalsController < ApplicationController
|
||||
def open
|
||||
@schedule = Schedule.new
|
||||
render partial: "partials/modals_open", locals: { type: params[:type], close_button: params[:close_button] }
|
||||
end
|
||||
end
|
||||
|
|
@ -1,37 +1,11 @@
|
|||
class ModbusController < ApplicationController
|
||||
def index
|
||||
@schedule = Schedule.all
|
||||
@modbus_running = Modbus::PollingService.running?
|
||||
end
|
||||
|
||||
def start
|
||||
Modbus::PollingService.start
|
||||
redirect_to modbus_index_path
|
||||
redirect_to schedules_path
|
||||
end
|
||||
|
||||
def stop
|
||||
Modbus::PollingService.stop
|
||||
redirect_to modbus_index_path
|
||||
end
|
||||
|
||||
def schedule_edit
|
||||
@schedule = Schedule.all
|
||||
end
|
||||
|
||||
def schedule_edit_update
|
||||
error_hours = []
|
||||
|
||||
params[:schedule].each do |id, attributes|
|
||||
schedule = Schedule.find_by(id: id)
|
||||
unless schedule.update(temperature: attributes[:temperature])
|
||||
error_hours << "#{schedule.hour}시"
|
||||
end
|
||||
end
|
||||
|
||||
if error_hours.any?
|
||||
redirect_to modbus_index_path, alert: "#{error_hours.join(', ')}의 온도 업데이트에 실패하였습니다."
|
||||
else
|
||||
redirect_to modbus_index_path, notice: "스케줄이 성공적으로 업데이트되었습니다."
|
||||
end
|
||||
redirect_to schedules_path
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
class SchedulesController < ApplicationController
|
||||
def index
|
||||
@schedule = Schedule.order(:hour, :minute)
|
||||
@modbus_running = Modbus::PollingService.running?
|
||||
end
|
||||
|
||||
def new
|
||||
@schedule = Schedule.new
|
||||
end
|
||||
|
||||
def create
|
||||
@schedule = Schedule.new(schedule_params)
|
||||
|
||||
if @schedule.save
|
||||
redirect_to schedule_edit_schedules_path, notice: "스케쥴이 추가 되었습니다."
|
||||
else
|
||||
error_messages = @schedule.errors.full_messages.join(", ")
|
||||
redirect_to schedule_edit_schedules_path, alert: "스케쥴 추가 실패: #{error_messages}"
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@schedule = Schedule.find_by(id: params[:id])
|
||||
|
||||
if @schedule.destroy
|
||||
redirect_to schedule_edit_schedules_path, notice: "스케줄이 삭제되었습니다."
|
||||
else
|
||||
error_messages = @schedule.errors.full_messages.join(", ")
|
||||
redirect_to schedule_edit_schedules_path, alert: "스케쥴 삭제 실패: #{error_messages}"
|
||||
end
|
||||
end
|
||||
|
||||
def reset
|
||||
Schedule.delete_all
|
||||
ActiveRecord::Base.connection.execute("DELETE FROM sqlite_sequence WHERE name='schedules'")
|
||||
Rails.application.load_seed
|
||||
|
||||
redirect_to schedules_path, notice: "스케줄이 초기화되었습니다."
|
||||
end
|
||||
|
||||
def schedule_edit
|
||||
@schedule = Schedule.order(:hour, :minute)
|
||||
end
|
||||
|
||||
def schedule_edit_update
|
||||
error_messages = []
|
||||
|
||||
params[:schedule].each do |id, attributes|
|
||||
schedule = Schedule.find_by(id: id)
|
||||
next unless schedule
|
||||
|
||||
unless schedule.update(
|
||||
hour: attributes[:hour],
|
||||
minute: attributes[:minute],
|
||||
is_active: attributes[:is_active],
|
||||
temperature: attributes[:temperature]
|
||||
)
|
||||
|
||||
error_detail = schedule.errors.full_messages.join(", ")
|
||||
time_label = "#{attributes[:hour]}시 #{attributes[:minute]}분"
|
||||
error_messages << "#{time_label} - #{error_detail}"
|
||||
end
|
||||
end
|
||||
|
||||
if error_messages.any?
|
||||
redirect_to schedules_path, alert: error_messages.join("<br>")
|
||||
else
|
||||
redirect_to schedules_path, notice: "스케줄이 성공적으로 업데이트되었습니다."
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def schedule_params
|
||||
params.require(:schedule).permit(:hour, :minute, :is_active, :temperature)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
class WebController < ApplicationController
|
||||
end
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
module DeviceHelper
|
||||
end
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
module ModalsHelper
|
||||
end
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
module SchedulesHelper
|
||||
end
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
module WebHelper
|
||||
end
|
||||
|
|
@ -7,5 +7,8 @@ import { application } from "./application"
|
|||
import HelloController from "./hello_controller"
|
||||
application.register("hello", HelloController)
|
||||
|
||||
import ModalsController from "./modals_controller"
|
||||
application.register("modals", ModalsController)
|
||||
|
||||
import TimerController from "./timer_controller"
|
||||
application.register("timer", TimerController)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
import { Controller } from "@hotwired/stimulus"
|
||||
import { useClickOutside } from 'stimulus-use'
|
||||
|
||||
// Connects to data-controller="modals"
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
// useClickOutside(this)
|
||||
this.boundHandleKeydown = this.handleKeydown.bind(this)
|
||||
document.addEventListener("keydown", this.boundHandleKeydown)
|
||||
window.addEventListener('modals:close', this.close.bind(this))
|
||||
|
||||
const panelElement = document.getElementById("modals-panel")
|
||||
const backdropElement = document.getElementById("modals-backdrop")
|
||||
|
||||
if (panelElement && backdropElement) {
|
||||
setTimeout(() => {
|
||||
panelElement.classList.remove('opacity-0', 'translate-y-4', 'sm:scale-95')
|
||||
panelElement.classList.add('opacity-100', 'translate-y-0', 'sm:scale-100')
|
||||
backdropElement.classList.remove('opacity-0')
|
||||
backdropElement.classList.add('opacity-75')
|
||||
}, 10) // 애니메이션이 바로 시작되도록 약간의 지연 추가
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
document.removeEventListener('keydown', this.boundHandleKeydown)
|
||||
}
|
||||
|
||||
handleKeydown(event) {
|
||||
if (event.key === "Escape") {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
||||
close(event) {
|
||||
const panelElement = document.getElementById("modals-panel")
|
||||
const backdropElement = document.getElementById("modals-backdrop")
|
||||
|
||||
if (panelElement && backdropElement) {
|
||||
panelElement.classList.remove("opacity-100", "translate-y-0", "sm:scale-100")
|
||||
panelElement.classList.add("opacity-0", "translate-y-4", "sm:scale-95")
|
||||
|
||||
backdropElement.classList.remove("opacity-75")
|
||||
backdropElement.classList.add("opacity-0")
|
||||
|
||||
setTimeout(() => {
|
||||
const modalsFrame = document.querySelector("turbo-frame#modals")
|
||||
modalsFrame.innerHTML = ""
|
||||
}, 300) // 닫힘 애니메이션 시간과 동일하게 300ms 지연 후 모달 제거
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Controller } from "@hotwired/stimulus"
|
||||
import {Controller} from "@hotwired/stimulus"
|
||||
|
||||
// Connects to data-controller="timer"
|
||||
export default class extends Controller {
|
||||
|
|
@ -15,8 +15,7 @@ export default class extends Controller {
|
|||
|
||||
updateTime() {
|
||||
const now = new Date()
|
||||
const formatted = `${now.getFullYear()}년 ${String(now.getMonth() + 1).padStart(2, '0')}월 ${String(now.getDate()).padStart(2, '0')}일 ` +
|
||||
this.outputTarget.textContent = `${now.getFullYear()}년 ${String(now.getMonth() + 1).padStart(2, '0')}월 ${String(now.getDate()).padStart(2, '0')}일 ` +
|
||||
`${String(now.getHours()).padStart(2, '0')}시 ${String(now.getMinutes()).padStart(2, '0')}분 ${String(now.getSeconds()).padStart(2, '0')}초`
|
||||
this.outputTarget.textContent = formatted
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
class Schedule < ApplicationRecord
|
||||
validates :hour, presence: { message: "시간을 입력하세요" }
|
||||
validates :minute, presence: { message: "분을 입력하세요" }
|
||||
validates :temperature,
|
||||
presence: true,
|
||||
numericality: true,
|
||||
format: { with: /\A\d+(\.\d)?\z/ }
|
||||
presence: { message: "온도를 입력하세요" },
|
||||
numericality: { message: "온도는 숫자여야 합니다" },
|
||||
format: { with: /\A\d+(\.\d)?\z/, message: "온도는 소수점 한 자리까지 입력 가능합니다" }
|
||||
validates :minute, uniqueness: { scope: :hour, message: "같은 시:분의 스케쥴이 이미 존재합니다" }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,46 +1,63 @@
|
|||
# polling_service.rb
|
||||
require "rufus-scheduler"
|
||||
|
||||
module Modbus
|
||||
class PollingService
|
||||
class << self
|
||||
def start
|
||||
return if $modbus_polling_threads.any?(&:alive?)
|
||||
return if @scheduler
|
||||
|
||||
thread = Thread.new do
|
||||
puts "[#{Time.current}] Modbus polling service 시작됨"
|
||||
last_logged_hour = nil
|
||||
loop do
|
||||
begin
|
||||
now = Time.now
|
||||
current_hour = now.hour
|
||||
puts "[#{Time.now}] Modbus polling service 시작됨 (Rufus)"
|
||||
@scheduler ||= Rufus::Scheduler.new
|
||||
|
||||
if current_hour != last_logged_hour && now.min == 0
|
||||
schedule = Schedule.find_by(hour: current_hour)
|
||||
|
||||
serial_path = Rails.root.join("serial.rb")
|
||||
system("ruby", serial_path.to_s, "#{schedule.temperature * 10}")
|
||||
|
||||
puts "[Schedule] #{current_hour}:00 -> Target temp: #{schedule.temperature}°C"
|
||||
last_logged_hour = current_hour
|
||||
end
|
||||
rescue StandardError => e
|
||||
error_message = "[#{Time.current}] 오류: #{e.message}"
|
||||
|
||||
puts error_message
|
||||
ensure
|
||||
sleep 0.1
|
||||
end
|
||||
end
|
||||
@scheduler.cron "0 * * * * *" do
|
||||
now = Time.now
|
||||
current_time = format("%02d:%02d", now.hour, now.min)
|
||||
puts "# current time: #{current_time}.#{now.sec}"
|
||||
schedule = Schedule.find_by(hour: now.hour, minute: now.min)
|
||||
apply_schedule(schedule) if schedule
|
||||
end
|
||||
$modbus_polling_threads << thread
|
||||
end
|
||||
|
||||
def stop
|
||||
$modbus_polling_threads.each { |t| t.kill if t.alive? }
|
||||
$modbus_polling_threads.clear
|
||||
puts "[#{Time.now}] Modbus polling service 중지됨"
|
||||
if defined?(@scheduler)
|
||||
@scheduler.shutdown(:kill)
|
||||
@scheduler = nil
|
||||
puts "[#{Time.now}] Modbus polling service 중지됨"
|
||||
else
|
||||
puts "[#{Time.now}] Scheduler 인스턴스 없음"
|
||||
end
|
||||
end
|
||||
|
||||
def running?
|
||||
$modbus_polling_threads.any?(&:alive?)
|
||||
@scheduler
|
||||
end
|
||||
|
||||
private
|
||||
def apply_schedule(schedule)
|
||||
if schedule.is_active
|
||||
run_script("on_off.rb", "0", "[Schedule] ON")
|
||||
sleep 2
|
||||
run_script(
|
||||
"serial.rb",
|
||||
(schedule.temperature * 10).to_i.to_s,
|
||||
"[Schedule] #{format('%02d:%02d', schedule.hour, schedule.minute)} → #{schedule.temperature}°C"
|
||||
)
|
||||
else
|
||||
run_script("on_off.rb", "1", "[Schedule] OFF")
|
||||
end
|
||||
end
|
||||
|
||||
def run_script(file, arg, success_msg)
|
||||
path = Rails.root.join(file)
|
||||
if system("ruby", path.to_s, arg)
|
||||
puts success_msg
|
||||
Rails.logger.info success_msg
|
||||
else
|
||||
error_message = "[#{Time.now}] #{file} 실행 실패 (args: #{arg})"
|
||||
puts error_message
|
||||
Rails.logger.error error_message
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
<div class="p-4">DEVICE</div>
|
||||
|
|
@ -23,12 +23,21 @@
|
|||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
|
||||
</head>
|
||||
|
||||
<body class="h-screen">
|
||||
<body>
|
||||
<%= turbo_frame_tag :modals %>
|
||||
<main class="flex flex-col h-full divide-y divide-border-table-border">
|
||||
<%= render "partials/header" %>
|
||||
<div class="flex flex-row flex-1 w-full divide-x divide-border-table-border">
|
||||
<%#= render "partials/sidebar" %>
|
||||
<div class="w-full h-full">
|
||||
<% if flash[:notice] %>
|
||||
<div class="m-4 rounded px-4 py-2 bg-accept text-white">
|
||||
<%= raw flash[:notice] %>
|
||||
</div>
|
||||
<% elsif flash[:alert] %>
|
||||
<div class="m-4 rounded px-4 py-2 bg-danger text-white">
|
||||
<%= raw flash[:alert] %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= yield %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= content_for(:title) || "Farmitry Hvac" %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
|
||||
<%= yield :head %>
|
||||
|
||||
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
||||
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
||||
|
||||
<link rel="icon" href="/icon.png" type="image/png">
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/icon.png">
|
||||
|
||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
|
||||
</head>
|
||||
|
||||
<body class="h-screen overflow-hidden bg-base-background text-base-text min-w-[400px] overflow-x-auto">
|
||||
<%= turbo_frame_tag :modals %>
|
||||
<main class="flex flex-col h-full">
|
||||
<%= render "partials/header" %>
|
||||
<div class="flex flex-row flex-1 w-full overflow-hidden">
|
||||
<%= render "partials/sidebar" %>
|
||||
<div class="flex-1 h-full pb-4 pr-4">
|
||||
<div class="flex-1 h-full content">
|
||||
<% if flash[:notice] %>
|
||||
<div class="m-4 rounded px-4 py-2 bg-accept text-white">
|
||||
<%= raw flash[:notice] %>
|
||||
</div>
|
||||
<% elsif flash[:alert] %>
|
||||
<div class="m-4 rounded px-4 py-2 bg-danger text-white">
|
||||
<%= raw flash[:alert] %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= yield %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= content_for(:title) || "Farmitry Hvac" %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
|
||||
<%= yield :head %>
|
||||
|
||||
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
||||
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
||||
|
||||
<link rel="icon" href="/icon.png" type="image/png">
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/icon.png">
|
||||
|
||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%= turbo_frame_tag :modals %>
|
||||
<main class="flex flex-col h-full divide-y divide-border-table-border">
|
||||
<%= render "partials/header" %>
|
||||
<div class="flex flex-row flex-1 w-full divide-x divide-border-table-border">
|
||||
<div class="w-full h-full">
|
||||
<% if flash[:notice] %>
|
||||
<div class="m-4 rounded px-4 py-2 bg-accept text-white">
|
||||
<%= raw flash[:notice] %>
|
||||
</div>
|
||||
<% elsif flash[:alert] %>
|
||||
<div class="m-4 rounded px-4 py-2 bg-danger text-white">
|
||||
<%= raw flash[:alert] %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= yield %>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<%= form_with url: schedule_edit_update_modbus_index_path, method: :post, class: 'flex flex-col h-full divide-y divide-border-table-border' do %>
|
||||
<div class="flex-1 p-4">
|
||||
<table class="base-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>시간</th>
|
||||
<th>온도</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @schedule.each do |s| %>
|
||||
<tr>
|
||||
<td><%= s.hour %>시</td>
|
||||
<td><%= number_field_tag "schedule[#{s.id}][temperature]", s.temperature, step: "0.1", inputmode: "decimal", class: "border px-2 py-1 rounded" %> °C</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flex p-4">
|
||||
<%= submit_tag "업데이트", class: "btn bg-primary" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
<header class="flex header" data-controller="timer">
|
||||
<div class="flex flex-row h-full">
|
||||
<%= image_tag "svg/svg_logo.svg", class: "w-16 h-auto" %>
|
||||
<div class="flex h-full items-center tracking-[0.5rem]">FARMITRY</div>
|
||||
<%= image_tag "svg/svg_logo.svg", class: "w-fit h-auto mr-4" %>
|
||||
<%= link_to root_path do %>
|
||||
<div class="flex h-full items-center tracking-[0.5rem]">FARMITRY</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div data-timer-target="output" class="text-gray-600"></div>
|
||||
<!-- <div class="flex flex-row items-center gap-x-4">-->
|
||||
<!-- <i class="fa-solid fa-circle-user text-4xl text-default-slate-dark leading-10"></i>-->
|
||||
<%#= link_to "로그아웃", '', class: "btn" %>
|
||||
<!-- </div>-->
|
||||
</header>
|
||||
<div data-timer-target="output"></div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
<%= turbo_frame_tag :modals do %>
|
||||
<div class="relative z-40" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<!--
|
||||
Background backdrop, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0"
|
||||
To: "opacity-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100"
|
||||
To: "opacity-0"
|
||||
-->
|
||||
<div id="modals-backdrop" class="fixed inset-0 bg-default-slate-dark transition-opacity opacity-0 ease-out duration-500" aria-hidden="true"></div>
|
||||
|
||||
<div class="fixed inset-0 z-40 w-screen overflow-y-auto py-8">
|
||||
<div class="flex min-h-full items-center justify-center text-center sm:items-center">
|
||||
<!--
|
||||
Modal panel, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
To: "opacity-100 translate-y-0 sm:scale-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100 translate-y-0 sm:scale-100"
|
||||
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
-->
|
||||
<div id="modals-panel" class="p-4 space-y-4 z-40 relative transform overflow-hidden rounded-md bg-white text-left shadow-xl transition-all w-full max-w-full mx-2 sm:mx-10 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95 ease-out duration-500" data-controller="modals" data-action="modals:click:outside->modals#close">
|
||||
<div>
|
||||
<%= render "partials/modals/#{type}" %>
|
||||
</div>
|
||||
<% if close_button != "false" %>
|
||||
<%= button_tag type: 'button',
|
||||
data: {
|
||||
action: 'modals#close'
|
||||
},
|
||||
class: 'inline-flex btn w-full' do %>
|
||||
<div class="items-center"><i class="fa-solid fa-square-caret-down"></i> 닫기</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<nav class="side-bar overflow-y-auto">
|
||||
<div class="menu-group">
|
||||
<div class="menu-group-icon">
|
||||
<i class="fa-solid fa-house text-default-slate-dark text-4xl"></i>
|
||||
<i class="fa-solid fa-house text-base-text/30 text-4xl"></i>
|
||||
</div>
|
||||
<div class="menu-group-name">
|
||||
HOME
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
<div class="menu-group">
|
||||
<div class="menu-group-icon">
|
||||
<i class="fa-solid fa-temperature-high text-default-slate-dark text-4xl"></i>
|
||||
<i class="fa-solid fa-temperature-high text-base-text/30 text-4xl"></i>
|
||||
</div>
|
||||
<div class="menu-group-name">
|
||||
메뉴 2
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
</div>
|
||||
<div class="menu-group">
|
||||
<div class="menu-group-icon">
|
||||
<i class="fa-solid fa-fan text-default-slate-dark text-4xl"></i>
|
||||
<i class="fa-solid fa-fan text-base-text/30 text-4xl"></i>
|
||||
</div>
|
||||
<div class="menu-group-name">
|
||||
메뉴 3
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
</div>
|
||||
<div class="menu-group">
|
||||
<div class="menu-group-icon">
|
||||
<i class="fa-solid fa-arrows-spin text-default-slate-dark text-4xl"></i>
|
||||
<i class="fa-solid fa-arrows-spin text-base-text/30 text-4xl"></i>
|
||||
</div>
|
||||
<div class="menu-group-name">
|
||||
메뉴 4
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
<div class="menu-group">
|
||||
<div class="menu-group-icon">
|
||||
<i class="fa-solid fa-gear text-default-slate-dark text-4xl"></i>
|
||||
<i class="fa-solid fa-gear text-base-text/30 text-4xl"></i>
|
||||
</div>
|
||||
<div class="menu-group-name">
|
||||
설청
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
<%= form_with model: @schedule, method: :post, data: { turbo: false }, class: 'flex flex-col h-full divide-y divide-border-table-border' do |form| %>
|
||||
<div class="flex-1 p-4">
|
||||
<table class="base-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>시간</th>
|
||||
<th>분</th>
|
||||
<th>사용여부</th>
|
||||
<th>온도</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<%= form.select :hour,
|
||||
options_for_select((0..23).map { |h| [h.to_s.rjust(2, '0'), h] }, @schedule.hour),
|
||||
{},
|
||||
class: "input-style" %>
|
||||
</td>
|
||||
<td>
|
||||
<%= form.number_field :minute,
|
||||
min: 0, max: 59, step: 1, inputmode: "decimal", class: "input-style" %>
|
||||
</td>
|
||||
<td>
|
||||
<%= check_box_tag "schedule[is_active]", "1", true %>
|
||||
</td>
|
||||
<td>
|
||||
<%= form.number_field :temperature,
|
||||
step: "0.1", inputmode: "decimal", class: "input-style" %>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flex p-4">
|
||||
<%= submit_tag "추가", class: "btn bg-primary" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -1,18 +1,11 @@
|
|||
<% if flash[:notice] %>
|
||||
<div class="m-4 rounded px-4 py-2 bg-accept text-white">
|
||||
<%= flash[:notice] %>
|
||||
</div>
|
||||
<% elsif flash[:alert] %>
|
||||
<div class="m-4 rounded px-4 py-2 bg-danger text-white">
|
||||
<%= flash[:alert] %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex flex-col h-full divide-y divide-border-table-border">
|
||||
<div class="flex flex-col flex-1 p-4 space-y-4">
|
||||
<table class="base-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>시간</th>
|
||||
<th>분</th>
|
||||
<th>사용여부</th>
|
||||
<th>온도</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -20,6 +13,8 @@
|
|||
<% @schedule.each do |s| %>
|
||||
<tr>
|
||||
<td><%= s.hour %>시</td>
|
||||
<td><%= s.minute %>분</td>
|
||||
<td><%= s.is_active %></td>
|
||||
<td><%= s.temperature %> °C</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
|
|
@ -42,6 +37,6 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="flex p-4">
|
||||
<%= link_to "수정", "/modbus/schedule_edit", class: "btn bg-default-slate" %>
|
||||
<%= link_to "수정", schedule_edit_schedules_path, class: "btn bg-default-slate" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<%= form_with url: schedule_edit_update_schedules_path, method: :post, class: 'flex flex-col h-full divide-y divide-border-table-border' do %>
|
||||
<div class="flex-1 p-4">
|
||||
<table class="base-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>시간</th>
|
||||
<th>분</th>
|
||||
<th>사용여부</th>
|
||||
<th>온도</th>
|
||||
<th>삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @schedule.each do |s| %>
|
||||
<tr>
|
||||
<td>
|
||||
<%= select_tag "schedule[#{s.id}][hour]",
|
||||
options_for_select((0..23).map { |h| [h.to_s.rjust(2, '0'), h] }, s.hour),
|
||||
class: "input-style" %>
|
||||
</td>
|
||||
<td><%= number_field_tag "schedule[#{s.id}][minute]", s.minute, min: 0, max: 59, step: 1, inputmode: "decimal", class: "input-style" %></td>
|
||||
<td><%= check_box_tag "schedule[#{s.id}][is_active]", "1", s.is_active == true || s.is_active == 1 %></td>
|
||||
<td><%= number_field_tag "schedule[#{s.id}][temperature]", s.temperature, step: "0.1", inputmode: "decimal", class: "input-style" %></td>
|
||||
<td>
|
||||
<%= link_to "삭제", schedule_path(s),
|
||||
data: {
|
||||
turbo_method: :delete,
|
||||
turbo_confirm: "정말 삭제하시겠습니까?"
|
||||
},
|
||||
class: "btn bg-danger text-sm" %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flex p-4">
|
||||
<%= submit_tag "업데이트", class: "btn bg-primary" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex p-4">
|
||||
<%= button_to "초기화", reset_schedules_path,
|
||||
method: :post,
|
||||
data: { turbo_confirm: "정말 초기화하시겠습니까? 모든 스케줄 데이터가 삭제됩니다." },
|
||||
class: "btn bg-danger" %>
|
||||
</div>
|
||||
<div class="flex p-4">
|
||||
<%= button_to "추가하기", open_modals_path(type: "add_schedule"),
|
||||
class: "btn bg-default-slate",
|
||||
data: {
|
||||
turbo_method: :post,
|
||||
turbo_frame: "modals"
|
||||
} %>
|
||||
</div>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<div>WEB</div>
|
||||
|
|
@ -28,5 +28,7 @@ module FarmitryHvac
|
|||
#
|
||||
# config.time_zone = "Central Time (US & Canada)"
|
||||
# config.eager_load_paths << Rails.root.join("extras")
|
||||
|
||||
config.time_zone = "Asia/Seoul"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,14 +10,30 @@ Rails.application.routes.draw do
|
|||
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
|
||||
|
||||
# Defines the root path route ("/")
|
||||
root "modbus#index"
|
||||
|
||||
root "schedules#index"
|
||||
|
||||
resources :schedules do
|
||||
collection do
|
||||
post "reset"
|
||||
get "schedule_edit"
|
||||
post "schedule_edit_update"
|
||||
end
|
||||
end
|
||||
|
||||
resources :modbus do
|
||||
collection do
|
||||
post "start"
|
||||
post "stop"
|
||||
get "schedule_edit"
|
||||
post "schedule_edit_update"
|
||||
end
|
||||
end
|
||||
|
||||
resources :modals do
|
||||
collection do
|
||||
post :open
|
||||
end
|
||||
end
|
||||
|
||||
resources :device
|
||||
resources :web
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ class CreateSchedules < ActiveRecord::Migration[8.0]
|
|||
def change
|
||||
create_table :schedules do |t|
|
||||
t.integer :hour
|
||||
t.integer :minute
|
||||
t.boolean :is_active
|
||||
t.float :temperature
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
add_index :schedules, [ :hour, :minute ], unique: true
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,8 +13,11 @@
|
|||
ActiveRecord::Schema[8.0].define(version: 2025_04_16_131440) do
|
||||
create_table "schedules", force: :cascade do |t|
|
||||
t.integer "hour"
|
||||
t.integer "minute"
|
||||
t.boolean "is_active"
|
||||
t.float "temperature"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["hour", "minute"], name: "index_schedules_on_hour_and_minute", unique: true
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,5 +9,5 @@
|
|||
# end
|
||||
|
||||
(0..23).each do | h |
|
||||
Schedule.create!(hour: h, temperature: 15.0)
|
||||
Schedule.create!(hour: h, minute: 0, is_active: true, temperature: 15.0)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
require "rmodbus"
|
||||
require "ccutrer-serialport"
|
||||
|
||||
value = ARGV[0]&.to_i
|
||||
|
||||
ModBus::RTUClient.new("/dev/ttyUSB0", 9600) do |cl|
|
||||
cl.with_slave(7) do |slave|
|
||||
regs = slave.holding_registers
|
||||
regs[22] = value
|
||||
sleep 0.1
|
||||
end
|
||||
end
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
"@hotwired/stimulus": "^3.2.2",
|
||||
"@hotwired/turbo-rails": "^8.0.13",
|
||||
"@tailwindcss/cli": "^4.1.4",
|
||||
"tailwindcss": "^4.1.4"
|
||||
"tailwindcss": "^4.1.4",
|
||||
"stimulus-use": "^0.52.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
require "test_helper"
|
||||
|
||||
class DeviceControllerTest < ActionDispatch::IntegrationTest
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
require "test_helper"
|
||||
|
||||
class ModalsControllerTest < ActionDispatch::IntegrationTest
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
require "test_helper"
|
||||
|
||||
class SchedulesControllerTest < ActionDispatch::IntegrationTest
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
require "test_helper"
|
||||
|
||||
class WebControllerTest < ActionDispatch::IntegrationTest
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
|
|
@ -571,6 +571,11 @@ picomatch@^2.3.1:
|
|||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
||||
stimulus-use@^0.52.2:
|
||||
version "0.52.3"
|
||||
resolved "https://registry.yarnpkg.com/stimulus-use/-/stimulus-use-0.52.3.tgz#d6f35fa93277274957a2ed98a7b04b4d702cb1d6"
|
||||
integrity sha512-stZ5dID6FUrGCR/ChWUa0FT5Z8iqkzT6lputOAb50eF+Ayg7RzJj4U/HoRlp2NV333QfvoRidru9HLbom4hZVw==
|
||||
|
||||
tailwindcss@4.1.4, tailwindcss@^4.1.4:
|
||||
version "4.1.4"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.1.4.tgz#27b3c910c6f1a47f4540451f3faf7cdd6d977a69"
|
||||
|
|
|
|||
Loading…
Reference in New Issue