manage overlays
This commit is contained in:
parent
17fca8fae5
commit
7ade38ecad
12 changed files with 177 additions and 23 deletions
|
|
@ -1,46 +1,77 @@
|
||||||
class OverlaysController < ApplicationController
|
class OverlaysController < ApplicationController
|
||||||
before_action :set_overlay, only: [ :destroy ]
|
before_action :set_overlay, only: [ :destroy ]
|
||||||
before_action :set_server_template, only: [ :create ]
|
before_action :set_server_template, only: [ :create, :destroy ]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@system_overlays = Overlay.system_overlays
|
@system_overlays = Overlay.system_overlays
|
||||||
@custom_overlays = current_user.overlays.custom_overlays.order(:name)
|
@custom_overlays = current_user.overlays.custom_overlays.order(:name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@overlay = current_user.overlays.build
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@server_template = current_user.server_templates.find(params[:server_template_id])
|
if @server_template.present?
|
||||||
@overlay = Overlay.find(params[:overlay_id])
|
# Attach existing overlay to a template
|
||||||
|
@overlay = Overlay.find(params[:overlay_id])
|
||||||
|
|
||||||
position = @server_template.template_overlays.maximum(:position).to_i + 1
|
position = @server_template.template_overlays.maximum(:position).to_i + 1
|
||||||
@template_overlay = @server_template.template_overlays.build(overlay_id: @overlay.id, position: position)
|
@template_overlay = @server_template.template_overlays.build(overlay_id: @overlay.id, position: position)
|
||||||
|
|
||||||
if @template_overlay.save
|
if @template_overlay.save
|
||||||
Activity.log(current_user, "added_overlay", "ServerTemplate", @server_template.id, { overlay: @overlay.name })
|
Activity.log(current_user, "added_overlay", "ServerTemplate", @server_template.id, { overlay: @overlay.name })
|
||||||
redirect_to @server_template, notice: "Overlay added successfully!"
|
redirect_to @server_template, notice: "Overlay added successfully!"
|
||||||
|
else
|
||||||
|
redirect_to @server_template, alert: "Failed to add overlay"
|
||||||
|
end
|
||||||
else
|
else
|
||||||
redirect_to @server_template, alert: "Failed to add overlay"
|
# Create a new custom overlay for the current user
|
||||||
|
@overlay = current_user.overlays.build(overlay_params.merge(overlay_type: "custom"))
|
||||||
|
|
||||||
|
if @overlay.save
|
||||||
|
Activity.log(current_user, "created_overlay", "Overlay", @overlay.id, { name: @overlay.name, slug: @overlay.slug })
|
||||||
|
redirect_to overlays_path, notice: "Overlay created"
|
||||||
|
else
|
||||||
|
render :new, status: :unprocessable_entity
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
authorize_user!
|
if @server_template.present?
|
||||||
server_template = @overlay.server_templates.find(params[:server_template_id])
|
authorize_user_for_template_overlay!
|
||||||
@overlay.template_overlays.where(server_template_id: server_template.id).destroy_all
|
@overlay.template_overlays.where(server_template_id: @server_template.id).destroy_all
|
||||||
Activity.log(current_user, "removed_overlay", "ServerTemplate", server_template.id, { overlay: @overlay.name })
|
Activity.log(current_user, "removed_overlay", "ServerTemplate", @server_template.id, { overlay: @overlay.name })
|
||||||
redirect_to server_template, notice: "Overlay removed successfully!"
|
redirect_to @server_template, notice: "Overlay removed successfully!"
|
||||||
|
else
|
||||||
|
authorize_owner!
|
||||||
|
name = @overlay.name
|
||||||
|
@overlay.destroy
|
||||||
|
redirect_to overlays_path, notice: "Overlay '#{name}' deleted"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_overlay
|
def set_overlay
|
||||||
@overlay = Overlay.find(params[:id])
|
@overlay = Overlay.find(params[:id]) if params[:id].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_server_template
|
def set_server_template
|
||||||
|
return unless params[:server_template_id].present?
|
||||||
@server_template = current_user.server_templates.find(params[:server_template_id])
|
@server_template = current_user.server_templates.find(params[:server_template_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def authorize_user!
|
def overlay_params
|
||||||
|
params.require(:overlay).permit(:name, :slug)
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorize_user_for_template_overlay!
|
||||||
redirect_to dashboard_path, alert: "Not authorized" unless @overlay.user_id.nil? || @overlay.user_id == current_user.id
|
redirect_to dashboard_path, alert: "Not authorized" unless @overlay.user_id.nil? || @overlay.user_id == current_user.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def authorize_owner!
|
||||||
|
redirect_to overlays_path, alert: "Not authorized" unless @overlay.user_id == current_user.id
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
class ConfigOption < ApplicationRecord
|
class ConfigOption < ApplicationRecord
|
||||||
belongs_to :server_template
|
belongs_to :server_template
|
||||||
|
|
||||||
validates :config_key, :config_value, presence: true
|
validates :config_key, presence: true
|
||||||
validates :config_key, uniqueness: { scope: :server_template_id }
|
validates :config_key, uniqueness: { scope: :server_template_id }
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,37 @@ class Overlay < ApplicationRecord
|
||||||
has_many :template_overlays, dependent: :destroy
|
has_many :template_overlays, dependent: :destroy
|
||||||
has_many :server_templates, through: :template_overlays
|
has_many :server_templates, through: :template_overlays
|
||||||
|
|
||||||
validates :name, :overlay_type, :path, presence: true
|
validates :name, :overlay_type, :slug, presence: true
|
||||||
validates :overlay_type, inclusion: { in: %w[system custom] }
|
validates :overlay_type, inclusion: { in: %w[system custom] }
|
||||||
validates :name, uniqueness: { scope: :user_id, message: "must be unique per user" }
|
validates :name, uniqueness: { scope: :user_id, message: "must be unique per user" }
|
||||||
|
validate :slug_is_single_directory
|
||||||
|
|
||||||
|
before_validation :normalize_slug
|
||||||
|
|
||||||
scope :system_overlays, -> { where(overlay_type: "system").where(user_id: nil) }
|
scope :system_overlays, -> { where(overlay_type: "system").where(user_id: nil) }
|
||||||
scope :custom_overlays, -> { where(overlay_type: "custom") }
|
scope :custom_overlays, -> { where(overlay_type: "custom") }
|
||||||
scope :for_user, ->(user) { where("user_id IS NULL OR user_id = ?", user.id) }
|
scope :for_user, ->(user) { where("user_id IS NULL OR user_id = ?", user.id) }
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def normalize_slug
|
||||||
|
return if slug.blank? && name.blank?
|
||||||
|
|
||||||
|
# Use provided slug or derive from name
|
||||||
|
source = slug.presence || name
|
||||||
|
self.slug = source.to_s.strip
|
||||||
|
.downcase
|
||||||
|
.gsub(%r{[^\w\-.]}, "_") # Replace non-word chars (except - and .) with underscore
|
||||||
|
.gsub(%r{_+}, "_") # Collapse multiple underscores
|
||||||
|
.sub(%r{^_+}, "") # Strip leading underscores
|
||||||
|
.sub(%r{_+$}, "") # Strip trailing underscores
|
||||||
|
end
|
||||||
|
|
||||||
|
def slug_is_single_directory
|
||||||
|
return if slug.blank?
|
||||||
|
|
||||||
|
if slug.match?(%r{[\\/]})
|
||||||
|
errors.add(:slug, "must be a single directory name (no slashes)")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
class StartupParam < ApplicationRecord
|
class StartupParam < ApplicationRecord
|
||||||
belongs_to :server_template
|
belongs_to :server_template
|
||||||
|
|
||||||
validates :param_key, :param_value, presence: true
|
validates :param_key, presence: true
|
||||||
validates :param_key, uniqueness: { scope: :server_template_id }
|
validates :param_key, uniqueness: { scope: :server_template_id }
|
||||||
end
|
end
|
||||||
|
|
|
||||||
44
app/views/layouts/application.html.erb
Normal file
44
app/views/layouts/application.html.erb
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title><%= content_for(:title) || "L4d Tools" %></title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="application-name" content="L4d Tools">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<%= csrf_meta_tags %>
|
||||||
|
<%= csp_meta_tag %>
|
||||||
|
|
||||||
|
<%= yield :head %>
|
||||||
|
|
||||||
|
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
||||||
|
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
||||||
|
|
||||||
|
<link rel="icon" href="/icon.png" type="image/png">
|
||||||
|
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||||
|
<link rel="apple-touch-icon" href="/icon.png">
|
||||||
|
|
||||||
|
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||||
|
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||||
|
<%= javascript_importmap_tags %>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header class="nav-bar" style="display:flex;gap:12px;align-items:center;padding:10px 16px;border-bottom:1px solid #ddd;margin-bottom:16px;">
|
||||||
|
<%= link_to "L4D.tools", dashboard_path, style: "font-weight:bold;" %>
|
||||||
|
<%= link_to "Servers", servers_path %>
|
||||||
|
<%= link_to "Templates", server_templates_path %>
|
||||||
|
<%= link_to "Overlays", overlays_path %>
|
||||||
|
<span style="margin-left:auto;"><%= link_to "Logout", logout_path %></span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<% if flash.any? %>
|
||||||
|
<div class="flash-messages">
|
||||||
|
<% flash.each do |type, message| %>
|
||||||
|
<div class="flash flash-<%= type %>"><%= message %></div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<%= yield %>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
24
app/views/overlays/index.html.slim
Normal file
24
app/views/overlays/index.html.slim
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
.overlays
|
||||||
|
h2 Overlays
|
||||||
|
= link_to "New Overlay", new_overlay_path, class: "btn btn--primary"
|
||||||
|
|
||||||
|
- all_overlays = (@system_overlays + @custom_overlays).sort_by(&:name)
|
||||||
|
- if all_overlays.any?
|
||||||
|
table
|
||||||
|
thead
|
||||||
|
tr
|
||||||
|
th Name
|
||||||
|
th Dir (slug)
|
||||||
|
th Type
|
||||||
|
th Actions
|
||||||
|
tbody
|
||||||
|
- all_overlays.each do |overlay|
|
||||||
|
tr
|
||||||
|
td = overlay.name
|
||||||
|
td = overlay.slug
|
||||||
|
td = overlay.overlay_type.humanize
|
||||||
|
td
|
||||||
|
- if overlay.overlay_type == "custom" && overlay.user_id == current_user.id
|
||||||
|
= link_to "Delete", overlay_path(overlay), method: :delete, data: { confirm: "Delete this overlay?" }, class: "btn btn--small btn--danger"
|
||||||
|
- else
|
||||||
|
p No overlays available.
|
||||||
22
app/views/overlays/new.html.slim
Normal file
22
app/views/overlays/new.html.slim
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
.overlay_form
|
||||||
|
h2 New Overlay
|
||||||
|
|
||||||
|
= form_with model: @overlay, url: overlays_path, local: true do |f|
|
||||||
|
- if @overlay.errors.any?
|
||||||
|
.alert.alert--error
|
||||||
|
h4 = pluralize(@overlay.errors.count, "error")
|
||||||
|
ul
|
||||||
|
- @overlay.errors.full_messages.each do |msg|
|
||||||
|
li = msg
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
= f.label :name
|
||||||
|
= f.text_field :name, placeholder: "e.g., custom_addons"
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
= f.label :slug, "Directory slug (optional, auto-derived from name)"
|
||||||
|
= f.text_field :slug, placeholder: "Leave blank to auto-generate"
|
||||||
|
|
||||||
|
.form-actions
|
||||||
|
= f.submit "Create Overlay", class: "btn btn--primary"
|
||||||
|
= link_to "Cancel", overlays_path, class: "btn"
|
||||||
|
|
@ -18,6 +18,8 @@
|
||||||
- @server_template.template_overlays.ordered.each do |to|
|
- @server_template.template_overlays.ordered.each do |to|
|
||||||
li
|
li
|
||||||
= to.overlay.name
|
= to.overlay.name
|
||||||
|
span.small
|
||||||
|
| (dir: /opt/l4d2/overlays/#{to.overlay.slug})
|
||||||
= link_to "Remove", server_template_overlay_path(@server_template, to.overlay), method: :delete, data: { confirm: "Sure?" }, class: "btn btn--small btn--danger"
|
= link_to "Remove", server_template_overlay_path(@server_template, to.overlay), method: :delete, data: { confirm: "Sure?" }, class: "btn btn--small btn--danger"
|
||||||
- else
|
- else
|
||||||
p No overlays selected.
|
p No overlays selected.
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ Rails.application.routes.draw do
|
||||||
resources :startup_params, only: [ :create, :destroy ]
|
resources :startup_params, only: [ :create, :destroy ]
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :overlays, only: [ :index ]
|
resources :overlays, only: [ :index, :new, :create, :destroy ]
|
||||||
|
|
||||||
resources :servers do
|
resources :servers do
|
||||||
member do
|
member do
|
||||||
|
|
|
||||||
5
db/migrate/20260118180000_rename_overlay_path_to_slug.rb
Normal file
5
db/migrate/20260118180000_rename_overlay_path_to_slug.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
class RenameOverlayPathToSlug < ActiveRecord::Migration[8.1]
|
||||||
|
def change
|
||||||
|
rename_column :overlays, :path, :slug
|
||||||
|
end
|
||||||
|
end
|
||||||
4
db/schema.rb
generated
4
db/schema.rb
generated
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.1].define(version: 2026_01_18_162630) do
|
ActiveRecord::Schema[8.1].define(version: 2026_01_18_180000) do
|
||||||
create_table "activities", force: :cascade do |t|
|
create_table "activities", force: :cascade do |t|
|
||||||
t.string "action", null: false
|
t.string "action", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
|
|
@ -38,7 +38,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_01_18_162630) do
|
||||||
t.text "description"
|
t.text "description"
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "overlay_type", default: "system", null: false
|
t.string "overlay_type", default: "system", null: false
|
||||||
t.string "path", null: false
|
t.string "slug", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.integer "user_id"
|
t.integer "user_id"
|
||||||
t.index ["user_id", "name"], name: "index_overlays_on_user_id_and_name", unique: true
|
t.index ["user_id", "name"], name: "index_overlays_on_user_id_and_name", unique: true
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ module L4dServer
|
||||||
|
|
||||||
def mount_overlayfs
|
def mount_overlayfs
|
||||||
server_dir = "/opt/l4d2/servers/#{@server.id}"
|
server_dir = "/opt/l4d2/servers/#{@server.id}"
|
||||||
overlays = @template.template_overlays.ordered.map { |to| "/opt/l4d2/overlays/#{to.overlay.name}" }.join(":")
|
overlays = @template.template_overlays.ordered.map { |to| "/opt/l4d2/overlays/#{to.overlay.slug}" }.join(":")
|
||||||
lower_dirs = "#{overlays}:/opt/l4d2/installation"
|
lower_dirs = "#{overlays}:/opt/l4d2/installation"
|
||||||
|
|
||||||
mount_cmd = "fuse-overlayfs -o lowerdir=#{lower_dirs},upperdir=#{server_dir}/upper,workdir=#{server_dir}/work #{server_dir}/merged"
|
mount_cmd = "fuse-overlayfs -o lowerdir=#{lower_dirs},upperdir=#{server_dir}/upper,workdir=#{server_dir}/work #{server_dir}/merged"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue