This commit is contained in:
2026-06-12 15:40:12 -04:00
commit 521653e5de
53 changed files with 8635 additions and 0 deletions
+17
View File
@@ -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"]
+40
View File
@@ -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
+87
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
require 'sinatra/activerecord/rake'
require_relative 'config/environment'
+1254
View File
File diff suppressed because one or more lines are too long
+152
View File
@@ -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
+14
View File
@@ -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
+26
View File
@@ -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
+14
View File
@@ -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
+15
View File
@@ -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
+57
View File
@@ -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
+23
View File
@@ -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>
+52
View File
@@ -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>
+135
View File
@@ -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>
+91
View File
@@ -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>
+177
View File
@@ -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>
+66
View File
@@ -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>
+13
View File
@@ -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>
+3
View File
@@ -0,0 +1,3 @@
<div class="small-12 columns">
<h2>404 Not Found</h2>
</div>
+3
View File
@@ -0,0 +1,3 @@
require_relative 'config/environment'
run ApplicationController
+15
View File
@@ -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
+13
View File
@@ -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
+53
View File
@@ -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
+27
View File
@@ -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:
+96
View File
@@ -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
+24
View File
@@ -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-----
+28
View File
@@ -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-----
+19
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
development: &default
http_auth_accounts:
admin: right_password
test:
<<: *default
production:
<<: *default
+14
View File
@@ -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
+26
View File
@@ -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
+15
View File
@@ -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'
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 60 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 60 KiB

View File
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

+337
View File
@@ -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

+338
View File
@@ -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

+70
View File
@@ -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>
+150
View File
@@ -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)
});
}); */
+309
View File
@@ -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 */
}
View File
+54
View File
@@ -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