init
@@ -0,0 +1,17 @@
|
|||||||
|
FROM ruby:3.3-slim
|
||||||
|
|
||||||
|
# Install dependencies needed for gems and mysql2
|
||||||
|
RUN apt-get update -qq && apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
default-libmysqlclient-dev \
|
||||||
|
git \
|
||||||
|
pkg-config
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY Gemfile Gemfile.lock ./
|
||||||
|
RUN bundle install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Start the Puma server
|
||||||
|
CMD ["bundle", "exec", "puma", "-p", "4567"]
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
|
gem 'sinatra', '~> 3.2.0'
|
||||||
|
gem 'sinatra-contrib', '~> 3.2.0'
|
||||||
|
gem 'activerecord', '~> 7.1'
|
||||||
|
gem 'sinatra-activerecord', '~> 2.0'
|
||||||
|
gem 'mysql2', '~> 0.5.3'
|
||||||
|
gem 'rake', '~> 13.0'
|
||||||
|
gem 'puma'
|
||||||
|
gem 'acts_as_list'
|
||||||
|
|
||||||
|
# gem "rake"
|
||||||
|
# gem 'sinatra', '~> 3.2'
|
||||||
|
# gem "activerecord"
|
||||||
|
# gem "activesupport"
|
||||||
|
# gem "sinatra-activerecord"
|
||||||
|
# gem "sinatra-contrib"
|
||||||
|
|
||||||
|
# gem "builder"
|
||||||
|
# gem "log4r"
|
||||||
|
|
||||||
|
# gem "mysql2"
|
||||||
|
# #gem "i18n"
|
||||||
|
|
||||||
|
# gem "sinatra-tailwind"
|
||||||
|
|
||||||
|
# gem "nokogiri", :require => false
|
||||||
|
|
||||||
|
group :development, :test do
|
||||||
|
gem "awesome_print"
|
||||||
|
|
||||||
|
# gem "rspec"
|
||||||
|
# gem "rspec-core"
|
||||||
|
# gem "fuubar"
|
||||||
|
# gem "ZenTest"
|
||||||
|
# gem "autotest-fsevent", "~> 0.2.9"
|
||||||
|
|
||||||
|
# gem "sqlite3"
|
||||||
|
# gem "ruby-debug19"
|
||||||
|
end
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
activemodel (7.2.3)
|
||||||
|
activesupport (= 7.2.3)
|
||||||
|
activerecord (7.2.3)
|
||||||
|
activemodel (= 7.2.3)
|
||||||
|
activesupport (= 7.2.3)
|
||||||
|
timeout (>= 0.4.0)
|
||||||
|
activesupport (7.2.3)
|
||||||
|
base64
|
||||||
|
benchmark (>= 0.3)
|
||||||
|
bigdecimal
|
||||||
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||||
|
connection_pool (>= 2.2.5)
|
||||||
|
drb
|
||||||
|
i18n (>= 1.6, < 2)
|
||||||
|
logger (>= 1.4.2)
|
||||||
|
minitest (>= 5.1)
|
||||||
|
securerandom (>= 0.3)
|
||||||
|
tzinfo (~> 2.0, >= 2.0.5)
|
||||||
|
acts_as_list (1.2.6)
|
||||||
|
activerecord (>= 6.1)
|
||||||
|
activesupport (>= 6.1)
|
||||||
|
awesome_print (1.9.2)
|
||||||
|
base64 (0.3.0)
|
||||||
|
benchmark (0.5.0)
|
||||||
|
bigdecimal (4.1.2)
|
||||||
|
concurrent-ruby (1.3.6)
|
||||||
|
connection_pool (3.0.2)
|
||||||
|
drb (2.2.3)
|
||||||
|
i18n (1.14.8)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
logger (1.7.0)
|
||||||
|
minitest (6.0.6)
|
||||||
|
drb (~> 2.0)
|
||||||
|
prism (~> 1.5)
|
||||||
|
multi_json (1.21.1)
|
||||||
|
mustermann (3.1.1)
|
||||||
|
mysql2 (0.5.7)
|
||||||
|
bigdecimal
|
||||||
|
nio4r (2.7.5)
|
||||||
|
prism (1.9.0)
|
||||||
|
puma (8.0.2)
|
||||||
|
nio4r (~> 2.0)
|
||||||
|
rack (2.2.23)
|
||||||
|
rack-protection (3.2.0)
|
||||||
|
base64 (>= 0.1.0)
|
||||||
|
rack (~> 2.2, >= 2.2.4)
|
||||||
|
rake (13.4.2)
|
||||||
|
securerandom (0.4.1)
|
||||||
|
sinatra (3.2.0)
|
||||||
|
mustermann (~> 3.0)
|
||||||
|
rack (~> 2.2, >= 2.2.4)
|
||||||
|
rack-protection (= 3.2.0)
|
||||||
|
tilt (~> 2.0)
|
||||||
|
sinatra-activerecord (2.0.28)
|
||||||
|
activerecord (>= 4.1)
|
||||||
|
sinatra (>= 1.0)
|
||||||
|
sinatra-contrib (3.2.0)
|
||||||
|
multi_json (>= 0.0.2)
|
||||||
|
mustermann (~> 3.0)
|
||||||
|
rack-protection (= 3.2.0)
|
||||||
|
sinatra (= 3.2.0)
|
||||||
|
tilt (~> 2.0)
|
||||||
|
tilt (2.7.0)
|
||||||
|
timeout (0.6.1)
|
||||||
|
tzinfo (2.0.6)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
aarch64-linux
|
||||||
|
ruby
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
activerecord (~> 7.1)
|
||||||
|
acts_as_list
|
||||||
|
awesome_print
|
||||||
|
mysql2 (~> 0.5.3)
|
||||||
|
puma
|
||||||
|
rake (~> 13.0)
|
||||||
|
sinatra (~> 3.2.0)
|
||||||
|
sinatra-activerecord (~> 2.0)
|
||||||
|
sinatra-contrib (~> 3.2.0)
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.5.22
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
require 'sinatra/activerecord/rake'
|
||||||
|
require_relative 'config/environment'
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
class ApplicationController < Sinatra::Base
|
||||||
|
register Sinatra::ActiveRecordExtension
|
||||||
|
register Sinatra::Contrib
|
||||||
|
|
||||||
|
configure do
|
||||||
|
set :views, File.expand_path('../../views', __FILE__)
|
||||||
|
set :public_folder, File.expand_path('../../../public', __FILE__)
|
||||||
|
set :root, File.expand_path('../../..', __FILE__)
|
||||||
|
set :trust_proxy, true
|
||||||
|
set :bind, '0.0.0.0'
|
||||||
|
set :port, 4567
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/' do
|
||||||
|
client_ip = request.ip.to_s
|
||||||
|
puts client_ip
|
||||||
|
|
||||||
|
if client_ip.start_with?('100.')
|
||||||
|
@url_type = 'tailscale'
|
||||||
|
elsif params[:local_ip].present? && params[:local_ip] == 'true'
|
||||||
|
@url_type = 'local_ip'
|
||||||
|
else
|
||||||
|
@url_type = 'local_domain'
|
||||||
|
end
|
||||||
|
@media = ServiceType.media
|
||||||
|
@media.services.map { |serv| serv.set_current_url(@url_type) }
|
||||||
|
@support = ServiceType.support
|
||||||
|
@support.services.map { |serv| serv.set_current_url(@url_type) }
|
||||||
|
@admin = ServiceType.admin
|
||||||
|
@admin.services.map { |serv| serv.set_current_url(@url_type) }
|
||||||
|
erb :'index'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin' do
|
||||||
|
erb :'admin/index'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin/local_network' do
|
||||||
|
@local_network = LocalNetwork.first || LocalNetwork.new
|
||||||
|
@is_readonly = true
|
||||||
|
erb :'admin/local_network/index'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin/local_network/edit' do
|
||||||
|
@local_network = LocalNetwork.first || LocalNetwork.create
|
||||||
|
@is_readonly = false
|
||||||
|
erb :'admin/local_network/index'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/admin/local_network' do
|
||||||
|
@local_network = LocalNetwork.find(params[:local_network][:id])
|
||||||
|
if @local_network.update(params[:local_network])
|
||||||
|
redirect 'admin/local_network'
|
||||||
|
else
|
||||||
|
erb :'admin/local_network/index'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin/machines' do
|
||||||
|
@machines = Machine.all
|
||||||
|
@is_readonly = true
|
||||||
|
erb :'admin/machines/index'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin/machines/edit' do
|
||||||
|
@machines = Machine.all
|
||||||
|
@is_readonly = false
|
||||||
|
erb :'admin/machines/index'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/admin/machines' do
|
||||||
|
params[:machines].each do |machine_data|
|
||||||
|
machine_data["local_network_id"] = 1
|
||||||
|
if machine_data[:id].empty? && machine_data[:name].present?
|
||||||
|
Machine.create(machine_data)
|
||||||
|
elsif machine_data[:id].present?
|
||||||
|
@machine = Machine.find(machine_data[:id])
|
||||||
|
@machine.update(machine_data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
redirect 'admin/machines'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin/service_types' do
|
||||||
|
@service_types = ServiceType.all
|
||||||
|
@is_readonly = true
|
||||||
|
erb :'admin/service_types/index'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin/service_types/edit' do
|
||||||
|
@service_types = ServiceType.all
|
||||||
|
@is_readonly = false
|
||||||
|
erb :'admin/service_types/index'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin/service_types/media' do
|
||||||
|
@service_type = ServiceType.media
|
||||||
|
erb :'admin/service_types/position'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin/service_types/admin' do
|
||||||
|
@service_type = ServiceType.admin
|
||||||
|
erb :'admin/service_types/position'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin/service_types/support' do
|
||||||
|
@service_type = ServiceType.support
|
||||||
|
erb :'admin/service_types/position'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/admin/service_types' do
|
||||||
|
puts params
|
||||||
|
params[:service_types].each do |service_type_data|
|
||||||
|
if service_type_data[:id].empty? && service_type_data[:name].present?
|
||||||
|
ServiceType.create(service_type_data)
|
||||||
|
elsif service_type_data[:id].present?
|
||||||
|
@service_type = ServiceType.find(service_type_data[:id])
|
||||||
|
@service_type.update(service_type_data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
redirect 'admin/service_types'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin/services' do
|
||||||
|
@services = Service.all
|
||||||
|
@service_types = ServiceType.all
|
||||||
|
@machines = Machine.all
|
||||||
|
@is_readonly = true
|
||||||
|
erb :'admin/services/index'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin/services/edit' do
|
||||||
|
@services = Service.all
|
||||||
|
@service_types = ServiceType.all
|
||||||
|
@machines = Machine.all
|
||||||
|
@is_readonly = false
|
||||||
|
erb :'admin/services/index'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/admin/services' do
|
||||||
|
params[:services].each do |service_data|
|
||||||
|
if service_data[:id].empty? && service_data[:name].present?
|
||||||
|
Service.create(service_data)
|
||||||
|
elsif service_data[:id].present?
|
||||||
|
@service = Service.find(service_data[:id])
|
||||||
|
@service.update(service_data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
redirect 'admin/services'
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# encoding: utf-8
|
||||||
|
|
||||||
|
class LocalNetwork < ActiveRecord::Base
|
||||||
|
# attr_accessible :title, :body
|
||||||
|
|
||||||
|
# validates :title, :presence => true
|
||||||
|
# validates :body, :presence => true
|
||||||
|
|
||||||
|
has_many :machines
|
||||||
|
# # belongs_to :someotherthing
|
||||||
|
|
||||||
|
# scope :published, -> { order("created_at DESC") }
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# encoding: utf-8
|
||||||
|
|
||||||
|
class Machine < ActiveRecord::Base
|
||||||
|
# attr_accessible :title, :body
|
||||||
|
|
||||||
|
# validates :title, :presence => true
|
||||||
|
# validates :body, :presence => true
|
||||||
|
|
||||||
|
has_many :services
|
||||||
|
belongs_to :local_network
|
||||||
|
|
||||||
|
# scope :published, -> { order("created_at DESC") }
|
||||||
|
|
||||||
|
# def tailscale_ip
|
||||||
|
# tailscale_ip
|
||||||
|
# end
|
||||||
|
|
||||||
|
def local_domain
|
||||||
|
"#{domain}.#{local_network.tld}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def local_ip
|
||||||
|
"#{local_network.subnet}.#{local_ip_octet}"
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# encoding: utf-8
|
||||||
|
|
||||||
|
class Post < ActiveRecord::Base
|
||||||
|
# attr_accessible :title, :body
|
||||||
|
|
||||||
|
# validates :title, :presence => true
|
||||||
|
# validates :body, :presence => true
|
||||||
|
|
||||||
|
# # has_many :somethings
|
||||||
|
# # belongs_to :someotherthing
|
||||||
|
|
||||||
|
# scope :published, -> { order("created_at DESC") }
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# encoding: utf-8
|
||||||
|
|
||||||
|
class ServiceType < ActiveRecord::Base
|
||||||
|
# attr_accessible :title, :body
|
||||||
|
|
||||||
|
# validates :title, :presence => true
|
||||||
|
# validates :body, :presence => true
|
||||||
|
|
||||||
|
has_many :services, -> { order(position: :asc) }
|
||||||
|
|
||||||
|
scope :media, -> { includes(:services).find_by(name: "Media") }
|
||||||
|
scope :support, -> { includes(:services).find_by(name: "Support") }
|
||||||
|
scope :admin, -> { includes(:services).find_by(name: "Admin") }
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# encoding: utf-8
|
||||||
|
|
||||||
|
class Service < ActiveRecord::Base
|
||||||
|
attr_accessor :current_url
|
||||||
|
|
||||||
|
# validates :title, :presence => true
|
||||||
|
# validates :body, :presence => true
|
||||||
|
|
||||||
|
belongs_to :machine
|
||||||
|
belongs_to :service_type
|
||||||
|
acts_as_list scope: :service_type
|
||||||
|
|
||||||
|
# scope :published, -> { order("created_at DESC") }
|
||||||
|
|
||||||
|
def tailscale_ip_url
|
||||||
|
if machine.tailscale_ip.present?
|
||||||
|
url = "http://#{machine.tailscale_ip}:#{port}"
|
||||||
|
if url_path.present?
|
||||||
|
url.concat(url_path)
|
||||||
|
end
|
||||||
|
url
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def local_domain_url
|
||||||
|
url = "http://"
|
||||||
|
if subdomain.present?
|
||||||
|
url.concat("#{subdomain}.")
|
||||||
|
end
|
||||||
|
url.concat(machine.local_domain)
|
||||||
|
if url_path.present?
|
||||||
|
url.concat(url_path)
|
||||||
|
end
|
||||||
|
url
|
||||||
|
end
|
||||||
|
|
||||||
|
def local_ip_url
|
||||||
|
url = "http://#{machine.local_ip}:#{port}"
|
||||||
|
if url_path.present?
|
||||||
|
url.concat(url_path)
|
||||||
|
end
|
||||||
|
url
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_current_url(url_type)
|
||||||
|
if url_type == 'tailscale'
|
||||||
|
self.current_url = self.tailscale_ip_url
|
||||||
|
elsif url_type == 'local_ip'
|
||||||
|
self.current_url = self.local_ip_url
|
||||||
|
else
|
||||||
|
self.current_url = self.local_domain_url
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<div class="text-4xl mb-10">
|
||||||
|
Network Directory Admin
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-1/2 flex flex-col space-y-10 ml-20 mt-10">
|
||||||
|
<a href="/admin/local_network" class="inline-block w-50 px-3 py-1 text-center font-semibold text-[#140029] bg-[#8000FF] border-2 border-[#8000FF] rounded-lg hover:text-[#2FD400] hover:bg-[#140029] hover:border-[#2FD400] transition-colors duration-200">
|
||||||
|
Local Network
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/admin/machines" class="inline-block w-50 px-3 py-1 text-center font-semibold text-[#140029] bg-[#8000FF] border-2 border-[#8000FF] rounded-lg hover:text-[#2FD400] hover:bg-[#140029] hover:border-[#2FD400] transition-colors duration-200">
|
||||||
|
Machines
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/admin/service_types" class="inline-block w-50 px-3 py-1 text-center font-semibold text-[#140029] bg-[#8000FF] border-2 border-[#8000FF] rounded-lg hover:text-[#2FD400] hover:bg-[#140029] hover:border-[#2FD400] transition-colors duration-200">
|
||||||
|
Service Types
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/admin/services" class="inline-block w-50 px-3 py-1 text-center font-semibold text-[#140029] bg-[#8000FF] border-2 border-[#8000FF] rounded-lg hover:text-[#2FD400] hover:bg-[#140029] hover:border-[#2FD400] transition-colors duration-200">
|
||||||
|
Services
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<form action="/admin/local_network" method="POST">
|
||||||
|
|
||||||
|
<div class="text-4xl mb-10">
|
||||||
|
Local Network Settings
|
||||||
|
</div>
|
||||||
|
<% unless @is_readonly %>
|
||||||
|
<div class="w-1/2 flex flex-none items-center justify-between ml-5 mb-2">
|
||||||
|
<div class="flex-none text-xl text-[#2FD400]">Edit</div>
|
||||||
|
<div class="grow h-[1px] mt-1 bg-[#2FD400]"></div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="flex ml-10 space-x-2">
|
||||||
|
|
||||||
|
<input type="hidden" name="local_network[id]" value="<%= @local_network.id %>">
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input class="h-8 border-2 border-[#8000FF] rounded-lg text-center" type="text" id="local_network_name" name="local_network[name]" value="<%= @local_network.name %>" <%= 'readonly' if @is_readonly %> >
|
||||||
|
<div class="text-[#2FD400]">Name</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input class="h-8 border-2 border-[#8000FF] rounded-lg text-center" type="text" id="local_network_subnet" name="local_network[subnet]" value="<%= @local_network.subnet %>" <%= 'readonly' if @is_readonly %> >
|
||||||
|
<div class="text-[#2FD400]">Subnet</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input class="h-8 border-2 border-[#8000FF] rounded-lg text-center" type="text" id="local_network_tld" name="local_network[tld]" value="<%= @local_network.tld %>" <%= 'readonly' if @is_readonly %> >
|
||||||
|
<div class="text-[#2FD400]">Top Level Domain</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col space-y-2 mt-10">
|
||||||
|
<% if @is_readonly %>
|
||||||
|
<div class="w-1/2 flex justify-end space-x-5">
|
||||||
|
<a href="/admin/local_network/edit" class="inline-block w-50 px-3 py-1 mt-20 text-center font-semibold text-[#140029] bg-[#8000FF] border-2 border-[#8000FF] rounded-lg hover:text-[#2FD400] hover:bg-[#140029] hover:border-[#2FD400] transition-colors duration-200">
|
||||||
|
Edit Local Network
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<div class="w-1/2 flex justify-end space-x-5">
|
||||||
|
<button type="submit" class="inline-block w-50 px-3 py-1 mt-20 text-center font-semibold text-[#140029] bg-[#8000FF] border-2 border-[#8000FF] rounded-lg hover:text-[#2FD400] hover:bg-[#140029] hover:border-[#2FD400] transition-colors duration-200">
|
||||||
|
Save Local Network
|
||||||
|
</button>
|
||||||
|
<a href="/admin/local_network" class="inline-block w-50 px-3 py-1 mt-20 text-center font-semibold text-[#140029] bg-[#F80800] border-2 border-[#F80800] rounded-lg hover:text-[#F80800] hover:bg-[#140029] hover:border-[#F80800] transition-colors duration-200">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<form action="/admin/machines" method="POST">
|
||||||
|
|
||||||
|
<div class="text-4xl mb-10">
|
||||||
|
Network Machines
|
||||||
|
</div>
|
||||||
|
<% unless @is_readonly %>
|
||||||
|
<div class="w-2/3 flex flex-none items-center justify-between ml-5 mb-2">
|
||||||
|
<div class="flex-none text-xl text-[#2FD400]">Edit</div>
|
||||||
|
<div class="grow h-[1px] mt-1 bg-[#2FD400]"></div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="flex flex-col ml-10 space-y-2">
|
||||||
|
<% @machines.each_with_index do |mach, index| %>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<input type="hidden" name="machines[][id]" value="<%= mach.id %>">
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="mach_<%= index %>_name"
|
||||||
|
name="machines[][name]"
|
||||||
|
value="<%= mach.name %>"
|
||||||
|
<%= 'readonly' if @is_readonly %> >
|
||||||
|
<% if index == @machines.length - 1 %>
|
||||||
|
<div class="text-[#2FD400]">Name</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="mach_<%= index %>_domain"
|
||||||
|
name="machines[][domain]"
|
||||||
|
value="<%= mach.domain %>"
|
||||||
|
<%= 'readonly' if @is_readonly %> >
|
||||||
|
<% if index == @machines.length - 1 %>
|
||||||
|
<div class="text-[#2FD400]">Domain</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="mach_<%= index %>_local_ip_octet"
|
||||||
|
name="machines[][local_ip_octet]"
|
||||||
|
value="<%= mach.local_ip_octet %>"
|
||||||
|
<%= 'readonly' if @is_readonly %> >
|
||||||
|
<% if index == @machines.length - 1 %>
|
||||||
|
<div class="text-[#2FD400]">Local IP Octet</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="mach_<%= index %>_tailscale_ip"
|
||||||
|
name="machines[][tailscale_ip]"
|
||||||
|
value="<%= mach.tailscale_ip %>"
|
||||||
|
<%= 'readonly' if @is_readonly %> >
|
||||||
|
<% if index == @machines.length - 1 %>
|
||||||
|
<div class="text-[#2FD400]">Tailscale IP</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex flex-col space-y-2 mt-10">
|
||||||
|
<% if @is_readonly %>
|
||||||
|
<div class="w-2/3 flex justify-end space-x-5">
|
||||||
|
<a href="/admin/machines/edit" class="inline-block w-50 px-3 py-1 mt-20 text-center font-semibold text-[#140029] bg-[#8000FF] border-2 border-[#8000FF] rounded-lg hover:text-[#2FD400] hover:bg-[#140029] hover:border-[#2FD400] transition-colors duration-200">
|
||||||
|
Edit/Add Machines
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<div class="w-2/3 flex flex-none items-center justify-between ml-5 mb-2">
|
||||||
|
<div class="flex-none text-xl text-[#2FD400]">New</div>
|
||||||
|
<div class="grow h-[1px] mt-1 bg-[#2FD400]"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-2 mb-10 ml-10">
|
||||||
|
<input type="hidden" name="machines[][id]" value="">
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="mach_new_name"
|
||||||
|
name="machines[][name]"
|
||||||
|
value=""
|
||||||
|
<%= 'readonly' if @is_readonly %> >
|
||||||
|
<label for="mach_new_name" class="text-[#2FD400]">Name</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="mach_new_domain"
|
||||||
|
name="machines[][domain]"
|
||||||
|
value=""
|
||||||
|
<%= 'readonly' if @is_readonly %> >
|
||||||
|
<label for="mach_new_domain" class="text-[#2FD400]">Domain</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="mach_new_local_ip_octet"
|
||||||
|
name="machines[][local_ip_octet]"
|
||||||
|
value=""
|
||||||
|
<%= 'readonly' if @is_readonly %> >
|
||||||
|
<label for="mach_new_local_ip_octet" class="text-[#2FD400]">Local IP Octet</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="mach_new_tailscale_ip"
|
||||||
|
name="machines[][tailscale_ip]"
|
||||||
|
value=""
|
||||||
|
<%= 'readonly' if @is_readonly %> >
|
||||||
|
<label for="mach_new_tailscale_ip" class="text-[#2FD400]">Tailscale IP</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-2/3 flex justify-end space-x-5">
|
||||||
|
<button type="submit" class="inline-block w-50 px-3 py-1 mt-20 text-center font-semibold text-[#140029] bg-[#8000FF] border-2 border-[#8000FF] rounded-lg hover:text-[#2FD400] hover:bg-[#140029] hover:border-[#2FD400] transition-colors duration-200">
|
||||||
|
Save Machines
|
||||||
|
</button>
|
||||||
|
<a href="/admin/machines" class="inline-block w-50 px-3 py-1 mt-20 text-center font-semibold text-[#140029] bg-[#F80800] border-2 border-[#F80800] rounded-lg hover:text-[#F80800] hover:bg-[#140029] hover:border-[#F80800] transition-colors duration-200">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<form action="/admin/service_types" method="POST">
|
||||||
|
|
||||||
|
<div class="text-4xl mb-10">
|
||||||
|
Service Types
|
||||||
|
</div>
|
||||||
|
<% unless @is_readonly %>
|
||||||
|
<div class="w-1/2 flex flex-none items-center justify-between ml-5 mb-2">
|
||||||
|
<div class="flex-none text-xl text-[#2FD400]">Edit</div>
|
||||||
|
<div class="grow h-[1px] mt-1 bg-[#2FD400]"></div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="flex flex-col ml-10 space-y-2">
|
||||||
|
<% @service_types.each_with_index do |servt, index| %>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<input type="hidden" name="service_types[][id]" value="<%= servt.id %>">
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="servt_<%= index %>_name"
|
||||||
|
name="service_types[][name]"
|
||||||
|
value="<%= servt.name %>"
|
||||||
|
<%= 'readonly' if @is_readonly %> >
|
||||||
|
<% if index == @service_types.length - 1 %>
|
||||||
|
<div class="text-[#2FD400]">Name</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="servt_<%= index %>_hex_color"
|
||||||
|
name="service_types[][hex_color]"
|
||||||
|
value="<%= servt.hex_color %>"
|
||||||
|
<%= 'readonly' if @is_readonly %> >
|
||||||
|
<% if index == @service_types.length - 1 %>
|
||||||
|
<div class="text-[#2FD400]">Hex Color</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex flex-col space-y-2 mt-10">
|
||||||
|
<% if @is_readonly %>
|
||||||
|
<div class="w-1/2 flex justify-end space-x-5">
|
||||||
|
<a href="/admin/service_types/edit" class="inline-block w-60 px-3 py-1 mt-20 text-center font-semibold text-[#140029] bg-[#8000FF] border-2 border-[#8000FF] rounded-lg hover:text-[#2FD400] hover:bg-[#140029] hover:border-[#2FD400] transition-colors duration-200">
|
||||||
|
Edit/Add Service Types
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<div class="w-1/2 flex flex-none items-center justify-between ml-5 mb-2">
|
||||||
|
<div class="flex-none text-xl text-[#2FD400]">New</div>
|
||||||
|
<div class="grow h-[1px] mt-1 bg-[#2FD400]"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-2 mb-10 ml-10">
|
||||||
|
<input type="hidden" name="service_types[][id]" value="">
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="servt_new_name"
|
||||||
|
name="service_types[][name]"
|
||||||
|
value=""
|
||||||
|
<%= 'readonly' if @is_readonly %> >
|
||||||
|
<label for="servt_new_name" class="text-[#2FD400]">Name</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="servt_new_hex_color"
|
||||||
|
name="service_types[][hex_color]"
|
||||||
|
value=""
|
||||||
|
<%= 'readonly' if @is_readonly %> >
|
||||||
|
<label for="servt_new_hex_color" class="text-[#2FD400]">Hex Color</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/2 flex justify-end space-x-5">
|
||||||
|
<button type="submit" class="inline-block w-50 px-3 py-1 mt-20 text-center font-semibold text-[#140029] bg-[#8000FF] border-2 border-[#8000FF] rounded-lg hover:text-[#2FD400] hover:bg-[#140029] hover:border-[#2FD400] transition-colors duration-200">
|
||||||
|
Save Service Types
|
||||||
|
</button>
|
||||||
|
<a href="/admin/service_types" class="inline-block w-50 px-3 py-1 mt-20 text-center font-semibold text-[#140029] bg-[#F80800] border-2 border-[#F80800] rounded-lg hover:text-[#F80800] hover:bg-[#140029] hover:border-[#F80800] transition-colors duration-200">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<form action="/admin/services" method="POST">
|
||||||
|
|
||||||
|
<div class="text-4xl mb-10">
|
||||||
|
Service Positions
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col ml-10 space-y-2">
|
||||||
|
<% @service_type.services.each_with_index do |service, index| %>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<input type="hidden" name="services[][id]" value="<%= service.id %>">
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="service_<%= index %>_name"
|
||||||
|
name="services[][name]"
|
||||||
|
value="<%= service.name %>"
|
||||||
|
readonly >
|
||||||
|
<% if index == @service_type.services.length - 1 %>
|
||||||
|
<div class="text-[#2FD400]">Name</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="service_<%= index %>_position"
|
||||||
|
name="services[][position]"
|
||||||
|
value="<%= service.position %>" >
|
||||||
|
<% if index == @service_type.services.length - 1 %>
|
||||||
|
<div class="text-[#2FD400]">Position</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="w-1/2 flex justify-end space-x-5">
|
||||||
|
<button type="submit" class="inline-block w-50 px-3 py-1 mt-20 text-center font-semibold text-[#140029] bg-[#8000FF] border-2 border-[#8000FF] rounded-lg hover:text-[#2FD400] hover:bg-[#140029] hover:border-[#2FD400] transition-colors duration-200">
|
||||||
|
Save Service Positions
|
||||||
|
</button>
|
||||||
|
<a href="/admin/service_types" class="inline-block w-50 px-3 py-1 mt-20 text-center font-semibold text-[#140029] bg-[#F80800] border-2 border-[#F80800] rounded-lg hover:text-[#F80800] hover:bg-[#140029] hover:border-[#F80800] transition-colors duration-200">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<form action="/admin/services" method="POST">
|
||||||
|
|
||||||
|
<div class="text-4xl mb-10">
|
||||||
|
Services
|
||||||
|
</div>
|
||||||
|
<% unless @is_readonly %>
|
||||||
|
<div class="w-19/20 flex flex-none items-center justify-between ml-5 mb-2">
|
||||||
|
<div class="flex-none text-xl text-[#2FD400]">Edit</div>
|
||||||
|
<div class="grow h-[1px] mt-1 bg-[#2FD400]"></div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="flex flex-col ml-10 space-y-2">
|
||||||
|
<% @services.each_with_index do |serv, index| %>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<input type="hidden" name="services[][id]" value="<%= serv.id %>">
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="serv_<%= index %>_name"
|
||||||
|
name="services[][name]"
|
||||||
|
value="<%= serv.name %>"
|
||||||
|
<%= 'readonly' if @is_readonly %> >
|
||||||
|
<% if index == @services.length - 1 %>
|
||||||
|
<div class="text-[#2FD400]">Name</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<select class="h-8 border-2 border-[#8000FF] rounded-lg text-center" name="services[][service_type_id]" id="service_type_select" <%= 'disabled' if @is_readonly %> >
|
||||||
|
<option value="">Select a Service Type</option>
|
||||||
|
<% @service_types.each do |service_type| %>
|
||||||
|
<option value="<%= service_type.id %>" <%= "selected" if serv.service_type_id == service_type.id %>><%= service_type.name %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
<% if index == @services.length - 1 %>
|
||||||
|
<div class="text-[#2FD400]">Service Type</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<select class="h-8 border-2 border-[#8000FF] rounded-lg text-center" name="services[][machine_id]" id="machine_select" <%= 'disabled' if @is_readonly %> >
|
||||||
|
<option value="">Select a Service Type</option>
|
||||||
|
<% @machines.each do |machine| %>
|
||||||
|
<option value="<%= machine.id %>" <%= "selected" if serv.machine_id == machine.id %>><%= machine.name %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
<% if index == @services.length - 1 %>
|
||||||
|
<div class="text-[#2FD400]">Machine</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="serv_<%= index %>_subdomain"
|
||||||
|
name="services[][subdomain]"
|
||||||
|
value="<%= serv.subdomain%>"
|
||||||
|
<%= 'readonly' if @is_readonly %> >
|
||||||
|
<% if index == @services.length - 1 %>
|
||||||
|
<div class="text-[#2FD400]">Subdomain</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="serv_<%= index %>_port"
|
||||||
|
name="services[][port]"
|
||||||
|
value="<%= serv.port%>"
|
||||||
|
<%= 'readonly' if @is_readonly %> >
|
||||||
|
<% if index == @services.length - 1 %>
|
||||||
|
<div class="text-[#2FD400]">Port</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="serv_<%= index %>_url_path"
|
||||||
|
name="services[][url_path]"
|
||||||
|
value="<%= serv.url_path%>"
|
||||||
|
<%= 'readonly' if @is_readonly %> >
|
||||||
|
<% if index == @services.length - 1 %>
|
||||||
|
<div class="text-[#2FD400]">URL Path (optional)</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex flex-col space-y-2 mt-10">
|
||||||
|
<% if @is_readonly %>
|
||||||
|
<div class="w-19/20 flex justify-end space-x-5">
|
||||||
|
<a href="/admin/services/edit" class="inline-block w-50 px-3 py-1 mt-20 text-center font-semibold text-[#140029] bg-[#8000FF] border-2 border-[#8000FF] rounded-lg hover:text-[#2FD400] hover:bg-[#140029] hover:border-[#2FD400] transition-colors duration-200">
|
||||||
|
Edit/Add Services
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<div class="w-19/20 flex flex-none items-center justify-between ml-5 mb-2">
|
||||||
|
<div class="flex-none text-xl text-[#2FD400]">New</div>
|
||||||
|
<div class="grow h-[1px] mt-1 bg-[#2FD400]"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-2 mb-10 ml-10"">
|
||||||
|
<input type="hidden" name="services[][id]" value="">
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="serv_new_name"
|
||||||
|
name="services[][name]"
|
||||||
|
value="">
|
||||||
|
<div class="text-[#2FD400]">Name</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<select class="h-8 border-2 border-[#8000FF] rounded-lg text-center" name="services[][service_type_id]" id="service_type_select" <%= 'disabled' if @is_readonly %> >
|
||||||
|
<option value="">Select a Service Type</option>
|
||||||
|
<% @service_types.each do |service_type| %>
|
||||||
|
<option value="<%= service_type.id %>"><%= service_type.name %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
<div class="text-[#2FD400]">Service Type</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<select class="h-8 border-2 border-[#8000FF] rounded-lg text-center" name="services[][machine_id]" id="machine_select" <%= 'disabled' if @is_readonly %> >
|
||||||
|
<option value="">Select a Service Type</option>
|
||||||
|
<% @machines.each do |machine| %>
|
||||||
|
<option value="<%= machine.id %>"><%= machine.name %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
<div class="text-[#2FD400]">Machine</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="serv_new_subdomain"
|
||||||
|
name="services[][subdomain]"
|
||||||
|
value="">
|
||||||
|
<div class="text-[#2FD400]">Subdomain</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="serv_new_port"
|
||||||
|
name="services[][port]"
|
||||||
|
value="">
|
||||||
|
<div class="text-[#2FD400]">Port</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<input type="text"
|
||||||
|
class="h-8 border-2 border-[#8000FF] rounded-lg text-center"
|
||||||
|
id="serv_new_url_path"
|
||||||
|
name="services[][url_path]"
|
||||||
|
value="">
|
||||||
|
<div class="text-[#2FD400]">URL Path (optional)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="w-19/20 flex justify-end space-x-5">
|
||||||
|
<button type="submit" class="inline-block w-50 px-3 py-1 mt-20 text-center font-semibold text-[#140029] bg-[#8000FF] border-2 border-[#8000FF] rounded-lg hover:text-[#2FD400] hover:bg-[#140029] hover:border-[#2FD400] transition-colors duration-200">
|
||||||
|
Save Services
|
||||||
|
</button>
|
||||||
|
<a href="/admin/services" class="inline-block w-50 px-3 py-1 mt-20 text-center font-semibold text-[#140029] bg-[#F80800] border-2 border-[#F80800] rounded-lg hover:text-[#F80800] hover:bg-[#140029] hover:border-[#F80800] transition-colors duration-200">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<div class="flex flex-col gap-y-2 bg-[url('/images/purpleandblackfire.svg')] bg-size-[auto_113%] bg-center bg-no-repeat">
|
||||||
|
<div class="flex flex-none items-center py-2 mb-13">
|
||||||
|
<div class="flex-none text-5xl text-[#8000FF]">Local Network Directory</div>
|
||||||
|
<div class="grow-5 h-[3px] mt-3 bg-[#8000FF]"></div>
|
||||||
|
<div class="flex-none mx-1 mt-2 text-2xl text-[#2FD400]">
|
||||||
|
<% if @url_type == 'tailscale' %>
|
||||||
|
<%= @url_type.gsub('_', ' ') %>
|
||||||
|
<% elsif @url_type == 'local_ip' %>
|
||||||
|
<a href="/" class="flex h-full text-center hover:text-[#009fff]">
|
||||||
|
<%= @url_type.gsub('_', ' ') %>
|
||||||
|
</a>
|
||||||
|
<% else %>
|
||||||
|
<a href="?local_ip=true" class="flex h-full text-center hover:text-[#009fff]">
|
||||||
|
<%= @url_type.gsub('_', ' ') %>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="grow-1 h-[3px] mt-3 bg-[#8000FF]"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex gap-x-7">
|
||||||
|
<div class="flex flex-col w-3/5 items-start">
|
||||||
|
<div class="flex bg-[#140029] text-center text-3xl text-[#<%= @media.hex_color %>] ml-15 -mb-4 px-2 z-1">
|
||||||
|
<%= @media.name %>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap text-2xl gap-1 py-4 justify-evenly items-center border-4 border-[#<%= @media.hex_color %>] rounded-lg">
|
||||||
|
<% @media.services.each do |service| %>
|
||||||
|
<a href=<%= service.current_url %> class="flex justify-center items-center h-15 w-55 px-2 py-10 my-4 text-center font-bold text-[#<%= @media.hex_color %>] bg-transparent border-2 border-[#<%= @media.hex_color %>] rounded-lg hover:text-[#140029] hover:bg-[#<%= @media.hex_color %>] transition-colors duration-200">
|
||||||
|
<%= service.name %>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-2/5 items-start">
|
||||||
|
<div class="bg-[#140029] text-center text-3xl text-[#<%= @support.hex_color %>] ml-15 -mb-4 px-2 z-1">
|
||||||
|
<%= @support.name %>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap text-2xl gap-1 py-4 justify-evenly items-center border-4 border-[#<%= @support.hex_color %>] rounded-lg">
|
||||||
|
<% @support.services.each do |service| %>
|
||||||
|
<a href=<%= service.current_url %> class="flex justify-center items-center h-15 w-55 px-2 py-10 my-4 text-center font-bold text-[#<%= @support.hex_color %>] bg-transparent border-2 border-[#<%= @support.hex_color %>] rounded-lg hover:text-[#140029] hover:bg-[#<%= @support.hex_color %>] transition-colors duration-200">
|
||||||
|
<%= service.name %>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full items-start">
|
||||||
|
<div class="bg-[#140029] text-center text-3xl text-[#<%= @admin.hex_color %>] ml-15 -mb-4 px-2 z-1">
|
||||||
|
<%= @admin.name %>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap w-full text-2xl gap-1 py-4 justify-evenly items-center border-4 border-[#<%= @admin.hex_color %>] rounded-lg">
|
||||||
|
<% @admin.services.each do |service| %>
|
||||||
|
<div class="flex w-[21%] justify-center items-center">
|
||||||
|
<a href=<%= service.current_url %> class="flex justify-center items-center h-15 w-55 px-2 py-10 my-4 text-center font-bold text-[#<%= @admin.hex_color %>] bg-transparent border-2 border-[#<%= @admin.hex_color %>] rounded-lg hover:text-[#140029] hover:bg-[#<%= @admin.hex_color %>] transition-colors duration-200">
|
||||||
|
<%= service.name %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Network Directory</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-[#140029] text-[#8000FF] font-semibold antialiased">
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<%= yield %>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div class="small-12 columns">
|
||||||
|
<h2>404 Not Found</h2>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
require_relative 'config/environment'
|
||||||
|
|
||||||
|
run ApplicationController
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
development:
|
||||||
|
adapter: mysql2
|
||||||
|
encoding: utf8mb4
|
||||||
|
database: dashboard_development
|
||||||
|
username: root
|
||||||
|
password: mypassword
|
||||||
|
host: db
|
||||||
|
port: 3306
|
||||||
|
|
||||||
|
production:
|
||||||
|
adapter: mysql2
|
||||||
|
encoding: utf8
|
||||||
|
username: root
|
||||||
|
password:
|
||||||
|
database: sinatra_template_production
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
require 'bundler/setup'
|
||||||
|
Bundler.require(:default, ENV['RACK_ENV'] || :development)
|
||||||
|
|
||||||
|
# Establish ActiveRecord Connection
|
||||||
|
ActiveRecord::Base.configurations = YAML.load_file('config/database.yml')
|
||||||
|
ActiveRecord::Base.establish_connection(ENV['RACK_ENV']&.to_sym || :development)
|
||||||
|
|
||||||
|
# Load Models
|
||||||
|
Dir.glob(File.join(File.dirname(__FILE__), '../app/models', '*.rb')).each { |file| require file }
|
||||||
|
require_relative '../app/controllers/application_controller'
|
||||||
|
|
||||||
|
# Enable ActiveRecord logging to stdout
|
||||||
|
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
class CreateLocalNetworks < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
create_table :local_networks do |t|
|
||||||
|
t.string :name
|
||||||
|
t.string :subnet
|
||||||
|
t.string :tld
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
class CreateMachines < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
create_table :machines do |t|
|
||||||
|
t.string :name
|
||||||
|
t.string :domain
|
||||||
|
t.string :local_ip_octet
|
||||||
|
t.string :tailscale_ip
|
||||||
|
|
||||||
|
t.belongs_to :local_network
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
class CreateServiceTypes < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
create_table :service_types do |t|
|
||||||
|
t.string :name
|
||||||
|
t.string :hex_color
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
class CreateServices < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
create_table :services do |t|
|
||||||
|
t.string :name
|
||||||
|
t.string :subdomain
|
||||||
|
t.string :port
|
||||||
|
t.string :url_path
|
||||||
|
t.integer :position
|
||||||
|
|
||||||
|
t.belongs_to :machine
|
||||||
|
t.belongs_to :service_type
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# This file is auto-generated from the current state of the database. Instead
|
||||||
|
# of editing this file, please use the migrations feature of Active Record to
|
||||||
|
# incrementally modify your database, and then regenerate this schema definition.
|
||||||
|
#
|
||||||
|
# This file is the source Rails uses to define your schema when running `bin/rails
|
||||||
|
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
||||||
|
# be faster and is potentially less error prone than running all of your
|
||||||
|
# migrations from scratch. Old migrations may fail to apply correctly if those
|
||||||
|
# migrations use external dependencies or application code.
|
||||||
|
#
|
||||||
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
|
ActiveRecord::Schema[7.2].define(version: 2026_05_27_212837) do
|
||||||
|
create_table "local_networks", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
|
t.string "name"
|
||||||
|
t.string "subnet"
|
||||||
|
t.string "tld"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "machines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
|
t.string "name"
|
||||||
|
t.string "domain"
|
||||||
|
t.string "local_ip_octet"
|
||||||
|
t.string "tailscale_ip"
|
||||||
|
t.bigint "local_network_id"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["local_network_id"], name: "index_machines_on_local_network_id"
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "service_types", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
|
t.string "name"
|
||||||
|
t.string "hex_color"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "services", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
|
t.string "name"
|
||||||
|
t.string "subdomain"
|
||||||
|
t.string "port"
|
||||||
|
t.string "url_path"
|
||||||
|
t.integer "position"
|
||||||
|
t.bigint "machine_id"
|
||||||
|
t.bigint "service_type_id"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["machine_id"], name: "index_services_on_machine_id"
|
||||||
|
t.index ["service_type_id"], name: "index_services_on_service_type_id"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
# command: bundle exec puma -p 4567 -b 0.0.0.0
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
ports:
|
||||||
|
- "4567:4567"
|
||||||
|
environment:
|
||||||
|
- RACK_ENV=development
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: mysql:8.0
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: mypassword
|
||||||
|
MYSQL_DATABASE: dashboard_development
|
||||||
|
ports:
|
||||||
|
- "3306:3306"
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/mysql
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
helpers do
|
||||||
|
def link_to title, url, &block
|
||||||
|
"<a href='#{url}'>#{title || yield}<a>"
|
||||||
|
end
|
||||||
|
|
||||||
|
def protected!
|
||||||
|
unless authorized?
|
||||||
|
response['WWW-Authenticate'] = %(Basic realm="Restricted Area")
|
||||||
|
throw(:halt, [401, "Not authorized\n"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorized?
|
||||||
|
@auth ||= Rack::Auth::Basic::Request.new(request.env)
|
||||||
|
@auth.provided? && @auth.basic? && @auth.credentials && @auth.credentials == [settings.http_auth_accounts.first[0], settings.http_auth_accounts.first[1]]
|
||||||
|
end
|
||||||
|
|
||||||
|
def in_groups_of(number, fill_with = nil)
|
||||||
|
if fill_with == false
|
||||||
|
collection = self
|
||||||
|
else
|
||||||
|
# size % number gives how many extra we have;
|
||||||
|
# subtracting from number gives how many to add;
|
||||||
|
# modulo number ensures we don't add group of just fill.
|
||||||
|
padding = (number - size % number) % number
|
||||||
|
collection = dup.concat([fill_with] * padding)
|
||||||
|
end
|
||||||
|
|
||||||
|
if block_given?
|
||||||
|
collection.each_slice(number) { |slice| yield(slice) }
|
||||||
|
else
|
||||||
|
returning [] do |groups|
|
||||||
|
collection.each_slice(number) { |group| groups << group }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def options_for_select(container, selected = nil)
|
||||||
|
return container if String === container
|
||||||
|
|
||||||
|
container = container.to_a if Hash === container
|
||||||
|
selected, disabled = extract_selected_and_disabled(selected).map do | r |
|
||||||
|
Array.wrap(r).map { |item| item.to_s }
|
||||||
|
end
|
||||||
|
|
||||||
|
container.map do |element|
|
||||||
|
html_attributes = option_html_attributes(element)
|
||||||
|
text, value = option_text_and_value(element).map { |item| item.to_s }
|
||||||
|
selected_attribute = ' selected="selected"' if option_value_selected?(value, selected)
|
||||||
|
disabled_attribute = ' disabled="disabled"' if disabled && option_value_selected?(value, disabled)
|
||||||
|
%(<option value="#{html_escape(value)}"#{selected_attribute}#{disabled_attribute}#{html_attributes}>#{html_escape(text)}</option>)
|
||||||
|
end.join("\n")
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_selected_and_disabled(selected)
|
||||||
|
if selected.is_a?(Proc)
|
||||||
|
[ selected, nil ]
|
||||||
|
else
|
||||||
|
selected = Array.wrap(selected)
|
||||||
|
options = selected.extract_options!.symbolize_keys
|
||||||
|
[ options.include?(:selected) ? options[:selected] : selected, options[:disabled] ]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def option_html_attributes(element)
|
||||||
|
return "" unless Array === element
|
||||||
|
html_attributes = []
|
||||||
|
element.select { |e| Hash === e }.reduce({}, :merge).each do |k, v|
|
||||||
|
html_attributes << " #{k}=\"#{ERB::Util.html_escape(v.to_s)}\""
|
||||||
|
end
|
||||||
|
html_attributes.join
|
||||||
|
end
|
||||||
|
|
||||||
|
def option_text_and_value(option)
|
||||||
|
# Options are [text, value] pairs or strings used for both.
|
||||||
|
case
|
||||||
|
when Array === option
|
||||||
|
option = option.reject { |e| Hash === e }
|
||||||
|
[option.first, option.last]
|
||||||
|
when !option.is_a?(String) && option.respond_to?(:first) && option.respond_to?(:last)
|
||||||
|
[option.first, option.last]
|
||||||
|
else
|
||||||
|
[option, option]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def option_value_selected?(value, selected)
|
||||||
|
if selected.respond_to?(:include?) && !selected.is_a?(String)
|
||||||
|
selected.include? value
|
||||||
|
else
|
||||||
|
value == selected
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIID/TCCAuWgAwIBAgIUdwbdfgvNHyMVmXLZYZpvunx9/AgwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwgY0xCzAJBgNVBAYTAlVTMRcwFQYDVQQIDA5Ob3J0aCBDYXJvbGluYTENMAsG
|
||||||
|
A1UEBwwEQ2FyeTEWMBQGA1UECgwNTGltZUdyZWVuRmlyZTEZMBcGA1UEAwwQY29k
|
||||||
|
ZWh1Yi5xbmFwLmxhbjEjMCEGCSqGSIb3DQEJARYUanRqb3JkYW4xM0BnbWFpbC5j
|
||||||
|
b20wHhcNMjYwNjEwMjA1OTU5WhcNMjcwNjEwMjA1OTU5WjCBjTELMAkGA1UEBhMC
|
||||||
|
VVMxFzAVBgNVBAgMDk5vcnRoIENhcm9saW5hMQ0wCwYDVQQHDARDYXJ5MRYwFAYD
|
||||||
|
VQQKDA1MaW1lR3JlZW5GaXJlMRkwFwYDVQQDDBBjb2RlaHViLnFuYXAubGFuMSMw
|
||||||
|
IQYJKoZIhvcNAQkBFhRqdGpvcmRhbjEzQGdtYWlsLmNvbTCCASIwDQYJKoZIhvcN
|
||||||
|
AQEBBQADggEPADCCAQoCggEBAMsGw5d7BOYXGIUr5ZkConyxdzwQE6S4pITvdO+3
|
||||||
|
oN++xe0V+s/cZmX7vVcp7Me4N9sJ9OSLoyfPIoP8dqbzpDRViMrqKzKwZJ3Let0W
|
||||||
|
wRbyRM/2+HeNaHgay3eQT876ciDyLz4IKHnw9gnW2mkCJh/ntboJQ63+uFKmOJZt
|
||||||
|
O1O8WDJtI+bqAyA+bX5LG8eGJYCjf3exdwsDQ/BBOApSCVxSWpGpaI7+N4jzrsOx
|
||||||
|
47cRaGHjTGXmX00oDewUIV2g3CWPf6r5dN5DFFQ8jOCeZ+GsZKPDXS0Q6jJY6DBp
|
||||||
|
fFKgv8jFufgv9K2oksXMIiuVgRcDzjwwzKRwGIBF/s6F2VECAwEAAaNTMFEwHQYD
|
||||||
|
VR0OBBYEFKlM5ts/wBM/aeT2K2hPbA1KfI6AMB8GA1UdIwQYMBaAFKlM5ts/wBM/
|
||||||
|
aeT2K2hPbA1KfI6AMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB
|
||||||
|
ALJiW6yuZ6fnP8vOKp0meND8079GQzU7tgGfsC1v1StQzQ4LMjAuoALKeLHmD4Ml
|
||||||
|
tLXtGFt0u3g6o5GfxU1Wxxn2rSGyeGurQU9vjgiJMKJHIRwSbEJVSGjMqyCTInjs
|
||||||
|
tt/FiKbO8OFJCBvEToc3GNrlW6ncLfAYNOSHprp2n7WJ4E8BxkKCR1ePAZbsds1g
|
||||||
|
ZMpJLnK8XFz9oUx9bx24XduKYDECEPSahq5FuRonSAKDemS8zmEV1PSrTmcen2SC
|
||||||
|
5EokOxRj7MkQpseOtROwr0F4Yq4mirZapAOO7ZWsxMjj2AWf1uF48nbi3/NqTMnN
|
||||||
|
Nx5FxOEatF1zyUeYdKDMz6s=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDLBsOXewTmFxiF
|
||||||
|
K+WZAqJ8sXc8EBOkuKSE73Tvt6DfvsXtFfrP3GZl+71XKezHuDfbCfTki6MnzyKD
|
||||||
|
/Ham86Q0VYjK6isysGSdy3rdFsEW8kTP9vh3jWh4Gst3kE/O+nIg8i8+CCh58PYJ
|
||||||
|
1tppAiYf57W6CUOt/rhSpjiWbTtTvFgybSPm6gMgPm1+SxvHhiWAo393sXcLA0Pw
|
||||||
|
QTgKUglcUlqRqWiO/jeI867DseO3EWhh40xl5l9NKA3sFCFdoNwlj3+q+XTeQxRU
|
||||||
|
PIzgnmfhrGSjw10tEOoyWOgwaXxSoL/Ixbn4L/StqJLFzCIrlYEXA848MMykcBiA
|
||||||
|
Rf7OhdlRAgMBAAECggEAFf4H9Koephp5HV7bjnswtPtn0DWpHRIjvyMtunRcnLQO
|
||||||
|
fl+/xSGppXEzKcSYuaswwL1HvfSK0kp/oYa45x2UC1e7G0jpsEJXidjDeLzI4oaQ
|
||||||
|
ker9r/ydRQpZATz8ii4KrBsz5x8s3EWv7zGrA8436UOZJbtwbYH+vzQyg8f2Edwu
|
||||||
|
+fhHDTiayXIgp38TwYNhzoakERadBLtyCUbypCrUOc+HYJlgbAzxrjNfLzJsM2Ht
|
||||||
|
9zvsLAylpkDiQuuKkPwVrpKWQkdcJnTzYyqRm5ydzlquF0ezEQEHfb5t/KFE7SVm
|
||||||
|
1FHbaY5qoXwcqhkQTohRe0frRQtdxsTmSDyw+MA3SQKBgQD0fwOqX+cGSdqEfENM
|
||||||
|
ZhBgw/244aOzqF3gYeByghcmsgzRUmwO/wuIfcql0xoHnFfJnFsrf0yzylXtMmre
|
||||||
|
xqwTmKcPPzJsuO9Y8JogwKcsh6VhMCWBkgw7P/XiT0BkhsCpLFTV+YZQ7veGDILs
|
||||||
|
lWlHFNAEmOiukiczMReUl1taqQKBgQDUlD3oc02mtX81h2k29SDjTmXq8fdY/Aug
|
||||||
|
A1d8ASuBPMS06qHDzMvemRkgkz76aDAd7M97kCfT8qBF9L/W01pAn2W/VyIFLpGK
|
||||||
|
kVReMJvVkRDPev+ljVa7GTWHSl9EircXtR0EtHy/CG6AzzPkToCACPnuSTFFOVrS
|
||||||
|
EzDOL+SaaQKBgQDuGCasqtniwNcAv7YV1yrJ4PLbMTjmwuYwlYAqYs9Cyo865OYA
|
||||||
|
MJS9papLk+k8Uh8XYaFTGZPLXhYReFCkg5qdNsIxUdy8Ddhfp2ag0Ju7/JirrWRI
|
||||||
|
6r3okR/U9FKD0soZtOckvOr1M9FuBA8Xb2TnaLguUe392qw76OnKtR6siQKBgQCn
|
||||||
|
HOKOGhaxN30JV6oeyhVQnBEC4bTQ/1MkN3xOv5yzvFHm54zDn/ukwjY+pYKc18r7
|
||||||
|
u25gdLLaq6HTXNRyzTPmGWijQpw79p/zjswEP7JB8giFEuxl+PZ1nxu1f4HlICdP
|
||||||
|
O9HUIQ7wHnDAUiM5F31tKaFQ8bkJ8kyzWOLFNGFCAQKBgQDpwiFdZZYfplmxuObp
|
||||||
|
vQVEmP1rIvkHzbS3hDEXRESi3/iok6XDTa5xBrZKTYHqJ+yaRsVhq6b3A3oC+Q5w
|
||||||
|
TMCJ7phrA5VAVHaEcf3QIPUc8UdkpUzDGpholN6+KulTJ7et6iszEv4BL8hTZFLv
|
||||||
|
hTQW4LBHzHk5awQjKMSBM1hlwg==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
development: &default
|
||||||
|
# general
|
||||||
|
default_timezone: "Asia/Taipei"
|
||||||
|
site_root_url: http://localhost:4567
|
||||||
|
per_page: 10
|
||||||
|
# meta tags
|
||||||
|
meta_title: Example Blog
|
||||||
|
meta_description: Sinatra Template is like a tiny Rails
|
||||||
|
meta_keywords: "sinatra ruby rails blog"
|
||||||
|
meta_og_site_name: Example Blog
|
||||||
|
meta_og_url: http://example.com/sub_dir
|
||||||
|
meta_og_image: http://example.com/sub_dir/images/og_image.png
|
||||||
|
|
||||||
|
test:
|
||||||
|
<<: *default
|
||||||
|
|
||||||
|
production:
|
||||||
|
<<: *default
|
||||||
|
site_root_url: http://example.com/sub_dir
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
development: &default
|
||||||
|
http_auth_accounts:
|
||||||
|
admin: right_password
|
||||||
|
test:
|
||||||
|
<<: *default
|
||||||
|
production:
|
||||||
|
<<: *default
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
development:
|
||||||
|
adapter: mysql2
|
||||||
|
encoding: utf8mb4
|
||||||
|
database: dashboard_development
|
||||||
|
username: root
|
||||||
|
password: mypassword
|
||||||
|
host: db
|
||||||
|
|
||||||
|
production:
|
||||||
|
adapter: mysql2
|
||||||
|
encoding: utf8
|
||||||
|
username: root
|
||||||
|
password:
|
||||||
|
database: sinatra_template_production
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
log4r_config:
|
||||||
|
loggers:
|
||||||
|
- name: app_logger
|
||||||
|
trace: "false"
|
||||||
|
outputters:
|
||||||
|
- stdout
|
||||||
|
- logfile
|
||||||
|
outputters:
|
||||||
|
- type: StdoutOutputter
|
||||||
|
name: stdout
|
||||||
|
level: DEBUG
|
||||||
|
formatter:
|
||||||
|
date_pattern: "%Y-%m-%d %H:%M:%S"
|
||||||
|
pattern: "%d [%l] %m"
|
||||||
|
type: PatternFormatter
|
||||||
|
|
||||||
|
- type: FileOutputter
|
||||||
|
name: logfile
|
||||||
|
level: INFO
|
||||||
|
date_pattern: "%Y%m%d"
|
||||||
|
trunc: "false"
|
||||||
|
filename: "#{HOME}/app_logger.log"
|
||||||
|
formatter:
|
||||||
|
date_pattern: "%Y-%m-%d %H:%M:%S"
|
||||||
|
pattern: "%d [%l] %m"
|
||||||
|
type: PatternFormatter
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
if ENV['MY_RUBY_HOME'] && ENV['MY_RUBY_HOME'].include?('rvm')
|
||||||
|
begin
|
||||||
|
rvm_path = File.dirname(File.dirname(ENV['MY_RUBY_HOME']))
|
||||||
|
rvm_lib_path = File.join(rvm_path, 'lib')
|
||||||
|
$LOAD_PATH.unshift rvm_lib_path
|
||||||
|
require 'rvm'
|
||||||
|
RVM.use_from_path! File.dirname(File.dirname(__FILE__))
|
||||||
|
rescue LoadError
|
||||||
|
raise "RVM ruby lib is currently unavailable."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# This assumes Bundler 1.0+
|
||||||
|
ENV['BUNDLE_GEMFILE'] = File.expand_path('../Gemfile', File.dirname(__FILE__))
|
||||||
|
require 'bundler/setup'
|
||||||
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 241 KiB |
@@ -0,0 +1,337 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="3001"
|
||||||
|
height="3001"
|
||||||
|
viewBox="0 0 3001 3001"
|
||||||
|
version="1.1"
|
||||||
|
id="svg52"
|
||||||
|
sodipodi:docname="greenswitch.svg"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940, 2025-05-08)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview52"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="false"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showborder="false"
|
||||||
|
borderlayer="false"
|
||||||
|
inkscape:antialias-rendering="true"
|
||||||
|
inkscape:zoom="0.14315544"
|
||||||
|
inkscape:cx="1896.5399"
|
||||||
|
inkscape:cy="1397.0828"
|
||||||
|
inkscape:window-width="1440"
|
||||||
|
inkscape:window-height="779"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="25"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg52" />
|
||||||
|
<defs
|
||||||
|
id="defs49">
|
||||||
|
<clipPath
|
||||||
|
id="clip-0">
|
||||||
|
<path
|
||||||
|
clip-rule="nonzero"
|
||||||
|
d="M 0.5 0.5 L 3000.5 0.5 L 3000.5 3000.5 L 0.5 3000.5 Z M 0.5 0.5 "
|
||||||
|
id="path1" />
|
||||||
|
</clipPath>
|
||||||
|
<radialGradient
|
||||||
|
id="radial-pattern-0"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
fx="0"
|
||||||
|
fy="0"
|
||||||
|
r="1500"
|
||||||
|
gradientTransform="matrix(1, 0, 0, 1, 1500.5, 1500.5)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="rgb(99.905396%, 99.90387%, 99.90387%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop1" />
|
||||||
|
<stop
|
||||||
|
offset="0.015625"
|
||||||
|
stop-color="rgb(99.64447%, 99.638367%, 99.639893%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop2" />
|
||||||
|
<stop
|
||||||
|
offset="0.0429688"
|
||||||
|
stop-color="rgb(99.311829%, 99.299622%, 99.302673%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop3" />
|
||||||
|
<stop
|
||||||
|
offset="0.0703125"
|
||||||
|
stop-color="rgb(98.979187%, 98.960876%, 98.965454%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop4" />
|
||||||
|
<stop
|
||||||
|
offset="0.0976562"
|
||||||
|
stop-color="rgb(98.64502%, 98.620605%, 98.626709%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop5" />
|
||||||
|
<stop
|
||||||
|
offset="0.125"
|
||||||
|
stop-color="rgb(98.312378%, 98.28186%, 98.28949%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop6" />
|
||||||
|
<stop
|
||||||
|
offset="0.152344"
|
||||||
|
stop-color="rgb(98.002625%, 97.966003%, 97.975159%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop7" />
|
||||||
|
<stop
|
||||||
|
offset="0.175781"
|
||||||
|
stop-color="rgb(97.717285%, 97.676086%, 97.686768%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop8" />
|
||||||
|
<stop
|
||||||
|
offset="0.199219"
|
||||||
|
stop-color="rgb(97.43042%, 97.384644%, 97.395325%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop9" />
|
||||||
|
<stop
|
||||||
|
offset="0.222656"
|
||||||
|
stop-color="rgb(97.146606%, 97.094727%, 97.106934%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop10" />
|
||||||
|
<stop
|
||||||
|
offset="0.246094"
|
||||||
|
stop-color="rgb(96.862793%, 96.80481%, 96.820068%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop11" />
|
||||||
|
<stop
|
||||||
|
offset="0.269531"
|
||||||
|
stop-color="rgb(96.600342%, 96.537781%, 96.554565%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop12" />
|
||||||
|
<stop
|
||||||
|
offset="0.289062"
|
||||||
|
stop-color="rgb(96.362305%, 96.295166%, 96.311951%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop13" />
|
||||||
|
<stop
|
||||||
|
offset="0.308594"
|
||||||
|
stop-color="rgb(96.122742%, 96.052551%, 96.069336%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop14" />
|
||||||
|
<stop
|
||||||
|
offset="0.328125"
|
||||||
|
stop-color="rgb(95.88623%, 95.811462%, 95.831299%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop15" />
|
||||||
|
<stop
|
||||||
|
offset="0.347656"
|
||||||
|
stop-color="rgb(95.648193%, 95.568848%, 95.588684%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop16" />
|
||||||
|
<stop
|
||||||
|
offset="0.367188"
|
||||||
|
stop-color="rgb(95.40863%, 95.326233%, 95.346069%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop17" />
|
||||||
|
<stop
|
||||||
|
offset="0.386719"
|
||||||
|
stop-color="rgb(95.196533%, 95.109558%, 95.13092%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop18" />
|
||||||
|
<stop
|
||||||
|
offset="0.402344"
|
||||||
|
stop-color="rgb(95.005798%, 94.915771%, 94.93866%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop19" />
|
||||||
|
<stop
|
||||||
|
offset="0.417969"
|
||||||
|
stop-color="rgb(94.813538%, 94.718933%, 94.743347%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop20" />
|
||||||
|
<stop
|
||||||
|
offset="0.433594"
|
||||||
|
stop-color="rgb(94.624329%, 94.526672%, 94.551086%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop21" />
|
||||||
|
<stop
|
||||||
|
offset="0.449219"
|
||||||
|
stop-color="rgb(94.43512%, 94.334412%, 94.358826%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop22" />
|
||||||
|
<stop
|
||||||
|
offset="0.464844"
|
||||||
|
stop-color="rgb(94.245911%, 94.140625%, 94.166565%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop23" />
|
||||||
|
<stop
|
||||||
|
offset="0.480469"
|
||||||
|
stop-color="rgb(94.05365%, 93.945312%, 93.971252%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop24" />
|
||||||
|
<stop
|
||||||
|
offset="0.496094"
|
||||||
|
stop-color="rgb(93.864441%, 93.753052%, 93.780518%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop25" />
|
||||||
|
<stop
|
||||||
|
offset="0.511719"
|
||||||
|
stop-color="rgb(93.603516%, 93.486023%, 93.515015%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop26" />
|
||||||
|
<stop
|
||||||
|
offset="0.539062"
|
||||||
|
stop-color="rgb(93.269348%, 93.145752%, 93.17627%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop27" />
|
||||||
|
<stop
|
||||||
|
offset="0.566406"
|
||||||
|
stop-color="rgb(92.961121%, 92.832947%, 92.86499%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop28" />
|
||||||
|
<stop
|
||||||
|
offset="0.589844"
|
||||||
|
stop-color="rgb(92.674255%, 92.539978%, 92.573547%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop29" />
|
||||||
|
<stop
|
||||||
|
offset="0.613281"
|
||||||
|
stop-color="rgb(92.388916%, 92.250061%, 92.285156%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop30" />
|
||||||
|
<stop
|
||||||
|
offset="0.636719"
|
||||||
|
stop-color="rgb(92.127991%, 91.984558%, 92.019653%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop31" />
|
||||||
|
<stop
|
||||||
|
offset="0.65625"
|
||||||
|
stop-color="rgb(91.888428%, 91.741943%, 91.778564%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop32" />
|
||||||
|
<stop
|
||||||
|
offset="0.675781"
|
||||||
|
stop-color="rgb(91.651917%, 91.500854%, 91.537476%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop33" />
|
||||||
|
<stop
|
||||||
|
offset="0.695312"
|
||||||
|
stop-color="rgb(91.436768%, 91.281128%, 91.319275%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop34" />
|
||||||
|
<stop
|
||||||
|
offset="0.710938"
|
||||||
|
stop-color="rgb(91.246033%, 91.087341%, 91.127014%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop35" />
|
||||||
|
<stop
|
||||||
|
offset="0.726562"
|
||||||
|
stop-color="rgb(91.056824%, 90.895081%, 90.934753%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop36" />
|
||||||
|
<stop
|
||||||
|
offset="0.742188"
|
||||||
|
stop-color="rgb(90.867615%, 90.701294%, 90.742493%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop37" />
|
||||||
|
<stop
|
||||||
|
offset="0.757812"
|
||||||
|
stop-color="rgb(90.603638%, 90.432739%, 90.475464%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop38" />
|
||||||
|
<stop
|
||||||
|
offset="0.785156"
|
||||||
|
stop-color="rgb(90.296936%, 90.119934%, 90.164185%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop39" />
|
||||||
|
<stop
|
||||||
|
offset="0.808594"
|
||||||
|
stop-color="rgb(90.008545%, 89.826965%, 89.872742%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop40" />
|
||||||
|
<stop
|
||||||
|
offset="0.832031"
|
||||||
|
stop-color="rgb(89.749146%, 89.561462%, 89.608765%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop41" />
|
||||||
|
<stop
|
||||||
|
offset="0.851562"
|
||||||
|
stop-color="rgb(89.535522%, 89.344788%, 89.39209%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop42" />
|
||||||
|
<stop
|
||||||
|
offset="0.867188"
|
||||||
|
stop-color="rgb(89.343262%, 89.149475%, 89.196777%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop43" />
|
||||||
|
<stop
|
||||||
|
offset="0.882812"
|
||||||
|
stop-color="rgb(89.083862%, 88.885498%, 88.934326%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop44" />
|
||||||
|
<stop
|
||||||
|
offset="0.910156"
|
||||||
|
stop-color="rgb(88.798523%, 88.594055%, 88.644409%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop45" />
|
||||||
|
<stop
|
||||||
|
offset="0.929688"
|
||||||
|
stop-color="rgb(88.581848%, 88.374329%, 88.426208%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop46" />
|
||||||
|
<stop
|
||||||
|
offset="0.945312"
|
||||||
|
stop-color="rgb(88.320923%, 88.108826%, 88.162231%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop47" />
|
||||||
|
<stop
|
||||||
|
offset="0.972656"
|
||||||
|
stop-color="rgb(87.986755%, 87.768555%, 87.823486%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop48" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="rgb(87.820435%, 87.599182%, 87.654114%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop49" />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
clip-path="url(#clip-0)"
|
||||||
|
id="g49"
|
||||||
|
style="fill:#000000;fill-opacity:0">
|
||||||
|
<path
|
||||||
|
fill-rule="nonzero"
|
||||||
|
fill="url(#radial-pattern-0)"
|
||||||
|
d="M 0.5 0.5 L 0.5 3000.5 L 3000.5 3000.5 L 3000.5 0.5 Z M 0.5 0.5 "
|
||||||
|
id="path49"
|
||||||
|
style="fill:#000000;fill-opacity:0" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke-width="1"
|
||||||
|
stroke-linecap="butt"
|
||||||
|
stroke-linejoin="miter"
|
||||||
|
stroke="#231f20"
|
||||||
|
stroke-opacity="1"
|
||||||
|
stroke-miterlimit="10"
|
||||||
|
d="M 3000.5,0.5 H 0.5 v 3000 h 3000 z m 0,0"
|
||||||
|
id="path50"
|
||||||
|
inkscape:label="path50"
|
||||||
|
style="display:inline;opacity:1" />
|
||||||
|
<path
|
||||||
|
fill-rule="nonzero"
|
||||||
|
fill="rgb(12.980652%, 11.320496%, 11.306763%)"
|
||||||
|
fill-opacity="1"
|
||||||
|
d="M 1505.5 536.078125 C 1471.878906 536.078125 1444.519531 563.441406 1444.519531 597.058594 C 1444.519531 630.679688 1471.878906 658.039062 1505.5 658.039062 C 1539.121094 658.039062 1566.480469 630.679688 1566.480469 597.058594 C 1566.480469 563.441406 1539.121094 536.078125 1505.5 536.078125 Z M 1505.5 719.011719 C 1438.25 719.011719 1383.550781 664.308594 1383.550781 597.058594 C 1383.550781 529.808594 1438.25 475.109375 1505.5 475.109375 C 1572.75 475.109375 1627.449219 529.808594 1627.449219 597.058594 C 1627.449219 664.308594 1572.75 719.011719 1505.5 719.011719 Z M 1212.820312 920.230469 C 1196.011719 920.230469 1182.328125 933.910156 1182.328125 950.71875 L 1182.328125 2048.28125 C 1182.328125 2065.089844 1196.011719 2078.769531 1212.820312 2078.769531 L 1798.179688 2078.769531 C 1814.988281 2078.769531 1828.671875 2065.089844 1828.671875 2048.28125 L 1828.671875 950.71875 C 1828.671875 933.910156 1814.988281 920.230469 1798.179688 920.230469 Z M 1798.179688 2139.738281 L 1212.820312 2139.738281 C 1162.378906 2139.738281 1121.351562 2098.71875 1121.351562 2048.28125 L 1121.351562 950.71875 C 1121.351562 900.28125 1162.378906 859.261719 1212.820312 859.261719 L 1798.179688 859.261719 C 1848.621094 859.261719 1889.648438 900.28125 1889.648438 950.71875 L 1889.648438 2048.28125 C 1889.648438 2098.71875 1848.621094 2139.738281 1798.179688 2139.738281 Z M 1505.5 2340.960938 C 1471.878906 2340.960938 1444.519531 2368.320312 1444.519531 2401.941406 C 1444.519531 2435.558594 1471.878906 2462.921875 1505.5 2462.921875 C 1539.121094 2462.921875 1566.480469 2435.558594 1566.480469 2401.941406 C 1566.480469 2368.320312 1539.121094 2340.960938 1505.5 2340.960938 Z M 1505.5 2523.890625 C 1438.25 2523.890625 1383.550781 2469.179688 1383.550781 2401.941406 C 1383.550781 2334.691406 1438.25 2279.988281 1505.5 2279.988281 C 1572.75 2279.988281 1627.449219 2334.691406 1627.449219 2401.941406 C 1627.449219 2469.179688 1572.75 2523.890625 1505.5 2523.890625 Z M 2243.300781 2749.5 L 767.695312 2749.5 C 683.640625 2749.5 615.257812 2681.121094 615.257812 2597.058594 L 615.257812 1444.621094 C 615.257812 1427.78125 628.902344 1414.128906 645.742188 1414.128906 C 662.582031 1414.128906 676.230469 1427.78125 676.230469 1444.621094 L 676.230469 2597.058594 C 676.230469 2647.5 717.257812 2688.519531 767.695312 2688.519531 L 2243.300781 2688.519531 C 2293.738281 2688.519531 2334.769531 2647.5 2334.769531 2597.058594 L 2334.769531 401.941406 C 2334.769531 351.5 2293.738281 310.480469 2243.300781 310.480469 L 767.695312 310.480469 C 717.257812 310.480469 676.230469 351.5 676.230469 401.941406 L 676.230469 1261.699219 C 676.230469 1278.53125 662.582031 1292.179688 645.742188 1292.179688 C 628.902344 1292.179688 615.257812 1278.53125 615.257812 1261.699219 L 615.257812 401.941406 C 615.257812 317.878906 683.640625 249.5 767.695312 249.5 L 2243.300781 249.5 C 2327.359375 249.5 2395.738281 317.878906 2395.738281 401.941406 L 2395.738281 2597.058594 C 2395.738281 2681.121094 2327.359375 2749.5 2243.300781 2749.5 "
|
||||||
|
id="path51"
|
||||||
|
style="fill:#32cd32;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
fill-rule="nonzero"
|
||||||
|
fill="rgb(12.980652%, 11.320496%, 11.306763%)"
|
||||||
|
fill-opacity="1"
|
||||||
|
d="m 1774.6846,1488.5956 h -524.3984 c -16.832,0 -30.4805,13.6523 -30.4805,30.4922 v 518.2891 h 585.3594 v -518.2891 c 0,-16.8399 -13.6484,-30.4922 -30.4805,-30.4922 z m -30.4883,60.9805 v 426.8203 h -463.4218 v -426.8203 h 463.4218"
|
||||||
|
id="path52"
|
||||||
|
style="fill:#32cd32;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,338 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="3001"
|
||||||
|
height="3001"
|
||||||
|
viewBox="0 0 3001 3001"
|
||||||
|
version="1.1"
|
||||||
|
id="svg52"
|
||||||
|
sodipodi:docname="purpleswitch.svg"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940, 2025-05-08)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview52"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="false"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showborder="false"
|
||||||
|
borderlayer="false"
|
||||||
|
inkscape:antialias-rendering="true"
|
||||||
|
inkscape:zoom="0.14315544"
|
||||||
|
inkscape:cx="1896.5399"
|
||||||
|
inkscape:cy="1397.0828"
|
||||||
|
inkscape:window-width="1440"
|
||||||
|
inkscape:window-height="779"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="25"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg52" />
|
||||||
|
<defs
|
||||||
|
id="defs49">
|
||||||
|
<clipPath
|
||||||
|
id="clip-0">
|
||||||
|
<path
|
||||||
|
clip-rule="nonzero"
|
||||||
|
d="M 0.5 0.5 L 3000.5 0.5 L 3000.5 3000.5 L 0.5 3000.5 Z M 0.5 0.5 "
|
||||||
|
id="path1" />
|
||||||
|
</clipPath>
|
||||||
|
<radialGradient
|
||||||
|
id="radial-pattern-0"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
fx="0"
|
||||||
|
fy="0"
|
||||||
|
r="1500"
|
||||||
|
gradientTransform="matrix(1, 0, 0, 1, 1500.5, 1500.5)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="rgb(99.905396%, 99.90387%, 99.90387%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop1" />
|
||||||
|
<stop
|
||||||
|
offset="0.015625"
|
||||||
|
stop-color="rgb(99.64447%, 99.638367%, 99.639893%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop2" />
|
||||||
|
<stop
|
||||||
|
offset="0.0429688"
|
||||||
|
stop-color="rgb(99.311829%, 99.299622%, 99.302673%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop3" />
|
||||||
|
<stop
|
||||||
|
offset="0.0703125"
|
||||||
|
stop-color="rgb(98.979187%, 98.960876%, 98.965454%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop4" />
|
||||||
|
<stop
|
||||||
|
offset="0.0976562"
|
||||||
|
stop-color="rgb(98.64502%, 98.620605%, 98.626709%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop5" />
|
||||||
|
<stop
|
||||||
|
offset="0.125"
|
||||||
|
stop-color="rgb(98.312378%, 98.28186%, 98.28949%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop6" />
|
||||||
|
<stop
|
||||||
|
offset="0.152344"
|
||||||
|
stop-color="rgb(98.002625%, 97.966003%, 97.975159%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop7" />
|
||||||
|
<stop
|
||||||
|
offset="0.175781"
|
||||||
|
stop-color="rgb(97.717285%, 97.676086%, 97.686768%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop8" />
|
||||||
|
<stop
|
||||||
|
offset="0.199219"
|
||||||
|
stop-color="rgb(97.43042%, 97.384644%, 97.395325%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop9" />
|
||||||
|
<stop
|
||||||
|
offset="0.222656"
|
||||||
|
stop-color="rgb(97.146606%, 97.094727%, 97.106934%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop10" />
|
||||||
|
<stop
|
||||||
|
offset="0.246094"
|
||||||
|
stop-color="rgb(96.862793%, 96.80481%, 96.820068%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop11" />
|
||||||
|
<stop
|
||||||
|
offset="0.269531"
|
||||||
|
stop-color="rgb(96.600342%, 96.537781%, 96.554565%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop12" />
|
||||||
|
<stop
|
||||||
|
offset="0.289062"
|
||||||
|
stop-color="rgb(96.362305%, 96.295166%, 96.311951%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop13" />
|
||||||
|
<stop
|
||||||
|
offset="0.308594"
|
||||||
|
stop-color="rgb(96.122742%, 96.052551%, 96.069336%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop14" />
|
||||||
|
<stop
|
||||||
|
offset="0.328125"
|
||||||
|
stop-color="rgb(95.88623%, 95.811462%, 95.831299%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop15" />
|
||||||
|
<stop
|
||||||
|
offset="0.347656"
|
||||||
|
stop-color="rgb(95.648193%, 95.568848%, 95.588684%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop16" />
|
||||||
|
<stop
|
||||||
|
offset="0.367188"
|
||||||
|
stop-color="rgb(95.40863%, 95.326233%, 95.346069%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop17" />
|
||||||
|
<stop
|
||||||
|
offset="0.386719"
|
||||||
|
stop-color="rgb(95.196533%, 95.109558%, 95.13092%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop18" />
|
||||||
|
<stop
|
||||||
|
offset="0.402344"
|
||||||
|
stop-color="rgb(95.005798%, 94.915771%, 94.93866%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop19" />
|
||||||
|
<stop
|
||||||
|
offset="0.417969"
|
||||||
|
stop-color="rgb(94.813538%, 94.718933%, 94.743347%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop20" />
|
||||||
|
<stop
|
||||||
|
offset="0.433594"
|
||||||
|
stop-color="rgb(94.624329%, 94.526672%, 94.551086%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop21" />
|
||||||
|
<stop
|
||||||
|
offset="0.449219"
|
||||||
|
stop-color="rgb(94.43512%, 94.334412%, 94.358826%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop22" />
|
||||||
|
<stop
|
||||||
|
offset="0.464844"
|
||||||
|
stop-color="rgb(94.245911%, 94.140625%, 94.166565%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop23" />
|
||||||
|
<stop
|
||||||
|
offset="0.480469"
|
||||||
|
stop-color="rgb(94.05365%, 93.945312%, 93.971252%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop24" />
|
||||||
|
<stop
|
||||||
|
offset="0.496094"
|
||||||
|
stop-color="rgb(93.864441%, 93.753052%, 93.780518%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop25" />
|
||||||
|
<stop
|
||||||
|
offset="0.511719"
|
||||||
|
stop-color="rgb(93.603516%, 93.486023%, 93.515015%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop26" />
|
||||||
|
<stop
|
||||||
|
offset="0.539062"
|
||||||
|
stop-color="rgb(93.269348%, 93.145752%, 93.17627%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop27" />
|
||||||
|
<stop
|
||||||
|
offset="0.566406"
|
||||||
|
stop-color="rgb(92.961121%, 92.832947%, 92.86499%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop28" />
|
||||||
|
<stop
|
||||||
|
offset="0.589844"
|
||||||
|
stop-color="rgb(92.674255%, 92.539978%, 92.573547%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop29" />
|
||||||
|
<stop
|
||||||
|
offset="0.613281"
|
||||||
|
stop-color="rgb(92.388916%, 92.250061%, 92.285156%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop30" />
|
||||||
|
<stop
|
||||||
|
offset="0.636719"
|
||||||
|
stop-color="rgb(92.127991%, 91.984558%, 92.019653%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop31" />
|
||||||
|
<stop
|
||||||
|
offset="0.65625"
|
||||||
|
stop-color="rgb(91.888428%, 91.741943%, 91.778564%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop32" />
|
||||||
|
<stop
|
||||||
|
offset="0.675781"
|
||||||
|
stop-color="rgb(91.651917%, 91.500854%, 91.537476%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop33" />
|
||||||
|
<stop
|
||||||
|
offset="0.695312"
|
||||||
|
stop-color="rgb(91.436768%, 91.281128%, 91.319275%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop34" />
|
||||||
|
<stop
|
||||||
|
offset="0.710938"
|
||||||
|
stop-color="rgb(91.246033%, 91.087341%, 91.127014%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop35" />
|
||||||
|
<stop
|
||||||
|
offset="0.726562"
|
||||||
|
stop-color="rgb(91.056824%, 90.895081%, 90.934753%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop36" />
|
||||||
|
<stop
|
||||||
|
offset="0.742188"
|
||||||
|
stop-color="rgb(90.867615%, 90.701294%, 90.742493%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop37" />
|
||||||
|
<stop
|
||||||
|
offset="0.757812"
|
||||||
|
stop-color="rgb(90.603638%, 90.432739%, 90.475464%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop38" />
|
||||||
|
<stop
|
||||||
|
offset="0.785156"
|
||||||
|
stop-color="rgb(90.296936%, 90.119934%, 90.164185%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop39" />
|
||||||
|
<stop
|
||||||
|
offset="0.808594"
|
||||||
|
stop-color="rgb(90.008545%, 89.826965%, 89.872742%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop40" />
|
||||||
|
<stop
|
||||||
|
offset="0.832031"
|
||||||
|
stop-color="rgb(89.749146%, 89.561462%, 89.608765%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop41" />
|
||||||
|
<stop
|
||||||
|
offset="0.851562"
|
||||||
|
stop-color="rgb(89.535522%, 89.344788%, 89.39209%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop42" />
|
||||||
|
<stop
|
||||||
|
offset="0.867188"
|
||||||
|
stop-color="rgb(89.343262%, 89.149475%, 89.196777%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop43" />
|
||||||
|
<stop
|
||||||
|
offset="0.882812"
|
||||||
|
stop-color="rgb(89.083862%, 88.885498%, 88.934326%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop44" />
|
||||||
|
<stop
|
||||||
|
offset="0.910156"
|
||||||
|
stop-color="rgb(88.798523%, 88.594055%, 88.644409%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop45" />
|
||||||
|
<stop
|
||||||
|
offset="0.929688"
|
||||||
|
stop-color="rgb(88.581848%, 88.374329%, 88.426208%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop46" />
|
||||||
|
<stop
|
||||||
|
offset="0.945312"
|
||||||
|
stop-color="rgb(88.320923%, 88.108826%, 88.162231%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop47" />
|
||||||
|
<stop
|
||||||
|
offset="0.972656"
|
||||||
|
stop-color="rgb(87.986755%, 87.768555%, 87.823486%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop48" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="rgb(87.820435%, 87.599182%, 87.654114%)"
|
||||||
|
stop-opacity="1"
|
||||||
|
id="stop49" />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
clip-path="url(#clip-0)"
|
||||||
|
id="g49"
|
||||||
|
style="fill:#000000;fill-opacity:0">
|
||||||
|
<path
|
||||||
|
fill-rule="nonzero"
|
||||||
|
fill="url(#radial-pattern-0)"
|
||||||
|
d="M 0.5 0.5 L 0.5 3000.5 L 3000.5 3000.5 L 3000.5 0.5 Z M 0.5 0.5 "
|
||||||
|
id="path49"
|
||||||
|
style="fill:#000000;fill-opacity:0" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke-width="10"
|
||||||
|
stroke-linecap="butt"
|
||||||
|
stroke-linejoin="miter"
|
||||||
|
stroke="#231f20"
|
||||||
|
stroke-opacity="1"
|
||||||
|
stroke-miterlimit="10"
|
||||||
|
d="M 30005,5 H 5 v 30000 h 30000 z m 0,0"
|
||||||
|
transform="matrix(0.1,0,0,-0.1,0,3001)"
|
||||||
|
id="path50"
|
||||||
|
inkscape:label="path50"
|
||||||
|
style="display:inline;opacity:1" />
|
||||||
|
<path
|
||||||
|
fill-rule="nonzero"
|
||||||
|
fill="rgb(12.980652%, 11.320496%, 11.306763%)"
|
||||||
|
fill-opacity="1"
|
||||||
|
d="M 1505.5 536.078125 C 1471.878906 536.078125 1444.519531 563.441406 1444.519531 597.058594 C 1444.519531 630.679688 1471.878906 658.039062 1505.5 658.039062 C 1539.121094 658.039062 1566.480469 630.679688 1566.480469 597.058594 C 1566.480469 563.441406 1539.121094 536.078125 1505.5 536.078125 Z M 1505.5 719.011719 C 1438.25 719.011719 1383.550781 664.308594 1383.550781 597.058594 C 1383.550781 529.808594 1438.25 475.109375 1505.5 475.109375 C 1572.75 475.109375 1627.449219 529.808594 1627.449219 597.058594 C 1627.449219 664.308594 1572.75 719.011719 1505.5 719.011719 Z M 1212.820312 920.230469 C 1196.011719 920.230469 1182.328125 933.910156 1182.328125 950.71875 L 1182.328125 2048.28125 C 1182.328125 2065.089844 1196.011719 2078.769531 1212.820312 2078.769531 L 1798.179688 2078.769531 C 1814.988281 2078.769531 1828.671875 2065.089844 1828.671875 2048.28125 L 1828.671875 950.71875 C 1828.671875 933.910156 1814.988281 920.230469 1798.179688 920.230469 Z M 1798.179688 2139.738281 L 1212.820312 2139.738281 C 1162.378906 2139.738281 1121.351562 2098.71875 1121.351562 2048.28125 L 1121.351562 950.71875 C 1121.351562 900.28125 1162.378906 859.261719 1212.820312 859.261719 L 1798.179688 859.261719 C 1848.621094 859.261719 1889.648438 900.28125 1889.648438 950.71875 L 1889.648438 2048.28125 C 1889.648438 2098.71875 1848.621094 2139.738281 1798.179688 2139.738281 Z M 1505.5 2340.960938 C 1471.878906 2340.960938 1444.519531 2368.320312 1444.519531 2401.941406 C 1444.519531 2435.558594 1471.878906 2462.921875 1505.5 2462.921875 C 1539.121094 2462.921875 1566.480469 2435.558594 1566.480469 2401.941406 C 1566.480469 2368.320312 1539.121094 2340.960938 1505.5 2340.960938 Z M 1505.5 2523.890625 C 1438.25 2523.890625 1383.550781 2469.179688 1383.550781 2401.941406 C 1383.550781 2334.691406 1438.25 2279.988281 1505.5 2279.988281 C 1572.75 2279.988281 1627.449219 2334.691406 1627.449219 2401.941406 C 1627.449219 2469.179688 1572.75 2523.890625 1505.5 2523.890625 Z M 2243.300781 2749.5 L 767.695312 2749.5 C 683.640625 2749.5 615.257812 2681.121094 615.257812 2597.058594 L 615.257812 1444.621094 C 615.257812 1427.78125 628.902344 1414.128906 645.742188 1414.128906 C 662.582031 1414.128906 676.230469 1427.78125 676.230469 1444.621094 L 676.230469 2597.058594 C 676.230469 2647.5 717.257812 2688.519531 767.695312 2688.519531 L 2243.300781 2688.519531 C 2293.738281 2688.519531 2334.769531 2647.5 2334.769531 2597.058594 L 2334.769531 401.941406 C 2334.769531 351.5 2293.738281 310.480469 2243.300781 310.480469 L 767.695312 310.480469 C 717.257812 310.480469 676.230469 351.5 676.230469 401.941406 L 676.230469 1261.699219 C 676.230469 1278.53125 662.582031 1292.179688 645.742188 1292.179688 C 628.902344 1292.179688 615.257812 1278.53125 615.257812 1261.699219 L 615.257812 401.941406 C 615.257812 317.878906 683.640625 249.5 767.695312 249.5 L 2243.300781 249.5 C 2327.359375 249.5 2395.738281 317.878906 2395.738281 401.941406 L 2395.738281 2597.058594 C 2395.738281 2681.121094 2327.359375 2749.5 2243.300781 2749.5 "
|
||||||
|
id="path51"
|
||||||
|
style="fill:#967bb6;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
fill-rule="nonzero"
|
||||||
|
fill="rgb(12.980652%, 11.320496%, 11.306763%)"
|
||||||
|
fill-opacity="1"
|
||||||
|
d="M 1767.699219 950.71875 L 1243.300781 950.71875 C 1226.46875 950.71875 1212.820312 964.371094 1212.820312 981.210938 L 1212.820312 1499.5 L 1798.179688 1499.5 L 1798.179688 981.210938 C 1798.179688 964.371094 1784.53125 950.71875 1767.699219 950.71875 Z M 1737.210938 1011.699219 L 1737.210938 1438.519531 L 1273.789062 1438.519531 L 1273.789062 1011.699219 L 1737.210938 1011.699219 "
|
||||||
|
id="path52"
|
||||||
|
style="fill:#967bb6;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,70 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Limegreenfire</title>
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
|
||||||
|
<script src="scripts.js" defer></script>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Tilt Neon">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<link rel="icon" href="./images/favicon.ico" type="image/x-icon">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="dashboard">
|
||||||
|
<div class="dashboard-header">
|
||||||
|
<div class="dashboard-title">LimegreenFire<span class="header-purple">Casa</span></div>
|
||||||
|
<div id="currentDashboard" class="current-dashboard neonText">
|
||||||
|
Local Network
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-field">
|
||||||
|
<div class="dashboard-item">
|
||||||
|
<a id="nginx" href="" class="button qnap">
|
||||||
|
Nginx
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-item">
|
||||||
|
<a id="qnas" href="" class="button qnap">
|
||||||
|
Qnas
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-item">
|
||||||
|
<a id="dockerRegistry" href="" class="button qnap">
|
||||||
|
Docker Image Registry
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-item">
|
||||||
|
<a id="jellyfin" href="" class="button qnap">
|
||||||
|
Jellyfin
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-item">
|
||||||
|
<a id="qtorrent" href="" class="button pc">
|
||||||
|
qTorrent
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-item">
|
||||||
|
<a id="radarr" href="" class="button pc">
|
||||||
|
Radarr
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-item">
|
||||||
|
<a id="sonarr" href="" class="button pc">
|
||||||
|
Sonarr
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-item">
|
||||||
|
<a id="lidarr" href="" class="button pc">
|
||||||
|
Lidarr
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-item">
|
||||||
|
<a id="gitRegistry" href="" class="button qnap">
|
||||||
|
Git Registry
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
function getAppIP( network, device, app ) {
|
||||||
|
let deviceIP;
|
||||||
|
let port;
|
||||||
|
|
||||||
|
console.log(network, " - ", device, " - ", app)
|
||||||
|
|
||||||
|
if (network == 'tailscale') {
|
||||||
|
deviceIP = (device == 'qnap') ? '100.97.249.88' : '100.115.109.63';
|
||||||
|
} else {
|
||||||
|
deviceIP = (device == 'qnap') ? '192.168.1.81' : '192.168.1.13';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (app) {
|
||||||
|
case 'nginx':
|
||||||
|
port = ":81";
|
||||||
|
break;
|
||||||
|
case 'qnas':
|
||||||
|
port = ":8080";
|
||||||
|
break;
|
||||||
|
case 'dockerRegistry':
|
||||||
|
port = ":5000";
|
||||||
|
break;
|
||||||
|
case 'jellyfin':
|
||||||
|
port = ":8096";
|
||||||
|
break;
|
||||||
|
case 'qtorrent':
|
||||||
|
port = ":8181";
|
||||||
|
break;
|
||||||
|
case 'radarr':
|
||||||
|
port = ":7878";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
port = ""
|
||||||
|
// case sonarr:
|
||||||
|
// port = ":"
|
||||||
|
// break;
|
||||||
|
// case lidarr:
|
||||||
|
// port = ":"
|
||||||
|
// break;
|
||||||
|
// case gitRegistry:
|
||||||
|
// port = ":"
|
||||||
|
// break;
|
||||||
|
};
|
||||||
|
|
||||||
|
return "http://" + deviceIP + port;
|
||||||
|
}
|
||||||
|
|
||||||
|
function propigateIps( network ) {
|
||||||
|
console.log('prop started: ', network)
|
||||||
|
const ipHasPortRegex = /^http:\/\/(\d{1,3}\.){3}\d{1,3}:\d+$/;
|
||||||
|
['qnap', 'pc'].forEach(function(deviceClass) {
|
||||||
|
$("." + deviceClass).each(function() {
|
||||||
|
let appName = $(this).attr("id");
|
||||||
|
let ipAddress = getAppIP(network, deviceClass, appName);
|
||||||
|
if (ipHasPortRegex.test(ipAddress)) {
|
||||||
|
$(this).attr('href', ipAddress);
|
||||||
|
} else {
|
||||||
|
$(this).removeAttr('href');
|
||||||
|
$(this).removeClass('button');
|
||||||
|
$(this).addClass('button-disabled');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function determineNetwork() {
|
||||||
|
const apiUrlTailscale = 'http://100.97.249.88:3030/';
|
||||||
|
try {
|
||||||
|
fetch(apiUrlTailscale);
|
||||||
|
console.log('determined: tailscale')
|
||||||
|
return 'tailscale';
|
||||||
|
} catch {
|
||||||
|
console.log('determined: local')
|
||||||
|
return'local';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fillIpsBasedOnNetwork() {
|
||||||
|
const apiUrlTailscale = 'http://100.97.249.88:3030/';
|
||||||
|
let network;
|
||||||
|
try {
|
||||||
|
await fetch(apiUrlTailscale);
|
||||||
|
console.log('determined: tailscale')
|
||||||
|
network = 'tailscale'
|
||||||
|
setHeader (network)
|
||||||
|
propigateIps(network);
|
||||||
|
} catch {
|
||||||
|
console.log('determined: local')
|
||||||
|
network = 'local'
|
||||||
|
setHeader (network)
|
||||||
|
propigateIps(network);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHeader( network ) {
|
||||||
|
const networkName = network.charAt(0).toUpperCase() + network.slice(1) + ' Network';
|
||||||
|
$("#currentDashboard").text(networkName);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
fillIpsBasedOnNetwork();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const changeDashboardButton = document.getElementById('changeDashboardButton');
|
||||||
|
|
||||||
|
changeDashboardButton.addEventListener('click', function() {
|
||||||
|
console.log("click")
|
||||||
|
$("#currentDashboard")
|
||||||
|
const currentDashboard = $("#currentDashboard")
|
||||||
|
|
||||||
|
let currentDashboardClasses = currentDashboard.attr('class');
|
||||||
|
let newNetwork = currentDashboardClasses.includes('tailscale') ? 'local' : 'tailscale';
|
||||||
|
|
||||||
|
['qnap', 'pc'].forEach(function(deviceClass) {
|
||||||
|
$("." + deviceClass).each(function() {
|
||||||
|
let appName = $(this).attr("id");
|
||||||
|
let ipAddress = getIP(newNetwork, deviceClass, appName);
|
||||||
|
if (ipRegex.test(ipAddress)) {
|
||||||
|
$(this).attr('href', ipAddress);
|
||||||
|
$(this).removeClass('button-disabled');
|
||||||
|
$(this).addClass('button');
|
||||||
|
} else {
|
||||||
|
$(this).removeAttr('href');
|
||||||
|
$(this).removeClass('button');
|
||||||
|
$(this).addClass('button-disabled');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
currentDashboard.toggleClass('local tailscale');
|
||||||
|
let networkName = newNetwork.charAt(0).toUpperCase() + newNetwork.slice(1) + ' Network';
|
||||||
|
currentDashboard.text(networkName)
|
||||||
|
|
||||||
|
const logoOn = $('#logoOn');
|
||||||
|
const logoOff = $('#logoOff');
|
||||||
|
let logoOnSrc = logoOn.attr('src');
|
||||||
|
let logoOffSrc = logoOff.attr('src');
|
||||||
|
|
||||||
|
logoOn.attr('src', logoOffSrc)
|
||||||
|
logoOff.attr('src', logoOnSrc)
|
||||||
|
});
|
||||||
|
}); */
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
background-image: url("./images/fullyblackfire.svg");
|
||||||
|
background-size: auto 113%; /* Zooms the image in by 50% relative to the container */
|
||||||
|
background-position: center 2px; /* Centers the zoomed image */
|
||||||
|
background-repeat: no-repeat; /* Prevents the image from repeating */
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1140px) {
|
||||||
|
body {
|
||||||
|
background-position: center 6px;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* @media (min-width: 1140px) {
|
||||||
|
body {
|
||||||
|
flex-basis: 30%;
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
body, html {
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: start;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1140px) {
|
||||||
|
.dashboard {
|
||||||
|
height: 95%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* @media (min-width: 1140px) {
|
||||||
|
.dashboard {
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
.dashboard-title {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
-webkit-text-stroke: 1px black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: start;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 46px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #32CD32;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-purple {
|
||||||
|
color: #967bb6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-dashboard {
|
||||||
|
color: black;
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: normal;
|
||||||
|
/* text-shadow: 4px 4px 10px #32CD32; */
|
||||||
|
/* text-shadow: 3px 3px 0 #32CD32;
|
||||||
|
text-shadow: -3px 3px 0 #32CD32;
|
||||||
|
text-shadow: -3px -3px 0 #32CD32;
|
||||||
|
text-shadow: 3px -3px 0 #32CD32; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.neonText {
|
||||||
|
animation: flicker 1.5s infinite alternate;
|
||||||
|
color: #fff;
|
||||||
|
font-family: "Tilt Neon"
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flicker {
|
||||||
|
0%, 18%, 22%, 25%, 53%, 57%, 100% {
|
||||||
|
text-shadow:
|
||||||
|
0 0 4px #fff,
|
||||||
|
0 0 11px #fff,
|
||||||
|
0 0 19px #fff,
|
||||||
|
0 0 40px #0fa,
|
||||||
|
0 0 80px #0fa,
|
||||||
|
0 0 90px #0fa,
|
||||||
|
0 0 100px #0fa,
|
||||||
|
0 0 150px #0fa;
|
||||||
|
}
|
||||||
|
20%, 24%, 55% {
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-field {
|
||||||
|
width: 80%;
|
||||||
|
height: 90%;
|
||||||
|
/* min-height: 62vh; */
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
border-style: double;
|
||||||
|
border-width: thick;
|
||||||
|
border-color: #32CD32;
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-row {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1000px) {
|
||||||
|
.dashboard-row {
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 10px 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-item {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 90%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
/* width: 90%; */
|
||||||
|
/* max-width: 364px; */
|
||||||
|
padding: 5px 0px;
|
||||||
|
/* padding: 0px 40px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .dashboard-item:only-child {
|
||||||
|
margin: 0 auto;
|
||||||
|
} */
|
||||||
|
|
||||||
|
@media (min-width: 760px) {
|
||||||
|
.dashboard-item {
|
||||||
|
flex-basis: 45%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1140px) {
|
||||||
|
.dashboard-item {
|
||||||
|
flex-basis: 30%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* @media (min-width: 1000px) {
|
||||||
|
.dashboard-item:only-child {
|
||||||
|
padding: 60px 0px 10px;
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
.single-item-row {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background-color: #32CD32;
|
||||||
|
color: black;
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px 32px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: medium;
|
||||||
|
border-color: black;
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background-color: black;
|
||||||
|
color: #967bb6;
|
||||||
|
border-color: #967bb6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-disabled {
|
||||||
|
background-color: dimgrey;
|
||||||
|
color: black;
|
||||||
|
border-color: black;
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px 32px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: medium;
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-button {
|
||||||
|
position: absolute; /* Positions the button relative to the viewport */
|
||||||
|
top: 40px;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000; /* Ensures the button appears above other content */
|
||||||
|
/* Add other styling for your button (e.g., background-color, padding, font-size) */
|
||||||
|
color: #967bb6;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 700px) {
|
||||||
|
.switch-button {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logoOff{
|
||||||
|
display:none
|
||||||
|
}
|
||||||
|
#logoOn{
|
||||||
|
display:block
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 8vh; /* Set your desired fixed height */
|
||||||
|
width: auto; /* Allow the width to adjust automatically */
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
height: 120px; /* Set your desired fixed height */
|
||||||
|
width: 350px; /* Allow the width to adjust automatically */
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
background-image: url("./images/greenfirewithpurple.svg");
|
||||||
|
height: 120px; /* Set your desired fixed height */
|
||||||
|
width: auto; /* Allow the width to adjust automatically */
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo:hover {
|
||||||
|
background-image: url("./images/fullyblackfire.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container img {
|
||||||
|
height: 100%; /* Image fills 100% of the container's height */
|
||||||
|
width: 100%; /* Allow width to adjust proportionally */
|
||||||
|
object-fit: contain; /* Crop and zoom to fill the container while maintaining aspect ratio */
|
||||||
|
/* display: block; Remove extra space below the image */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1000px) {
|
||||||
|
.image-container img {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch {
|
||||||
|
position: absolute; /* Position relative to the parent */
|
||||||
|
top: 0; /* Align to the top edge */
|
||||||
|
right: 0; /* Align to the right edge */
|
||||||
|
/* Other styles for your toggle switch */
|
||||||
|
}
|
||||||
|
|
||||||
|
.green-flame {
|
||||||
|
background-color: #000000; /* Light background color */
|
||||||
|
}
|
||||||
|
|
||||||
|
.black-flame{
|
||||||
|
background-color: #32CD32; /* Dark background color */
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Network Dashboard (Sinatra Edition)
|
||||||
|
|
||||||
|
## Prep for development
|
||||||
|
* Dockerize for dev
|
||||||
|
* Move over css and images
|
||||||
|
* Remove @post/template example stuff
|
||||||
|
|
||||||
|
|
||||||
|
## Models
|
||||||
|
|
||||||
|
### Network
|
||||||
|
* Has many devices
|
||||||
|
* Type (Local, Tailscale, External?)
|
||||||
|
|
||||||
|
### Device
|
||||||
|
* belongs to network
|
||||||
|
* has many applications
|
||||||
|
* Name (PC, Qnas, ect...)
|
||||||
|
* IP
|
||||||
|
|
||||||
|
### Application
|
||||||
|
* belongs to device
|
||||||
|
* Name (Jellyfin, Nginx, ect...)
|
||||||
|
* Port
|
||||||
|
* Subdomain
|
||||||
|
* Type
|
||||||
|
|
||||||
|
|
||||||
|
## Views
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
* Replicate html/js/css apache dashboard look
|
||||||
|
* Replace html/js with erb, move over css
|
||||||
|
* Media Buttons (green), ? Buttons (purple), Admin Buttons (red) grouped by type
|
||||||
|
* Alt view where apps ordered by device
|
||||||
|
|
||||||
|
### Update
|
||||||
|
* Require login
|
||||||
|
* Choose Edit/Update/Delete/Reorder for Application (Network and Device can stay in code)
|
||||||
|
* Form to do the requested action
|
||||||
|
|
||||||
|
### Queue Radarr
|
||||||
|
* Replicate logic in shortcut to queue item to Radarr
|
||||||
|
* Put all in one form
|
||||||
|
|
||||||
|
### Status and Logs Dash (docker app to do this?)
|
||||||
|
* Each app with current status
|
||||||
|
* Logs from apps where it makes sense
|
||||||
|
|
||||||
|
|
||||||
|
## Prep for use
|
||||||
|
* Remove log4r (look up what it does first)
|
||||||
|
* Remove testing stuff
|
||||||
|
* Dockerize for production
|
||||||