manage overlays

This commit is contained in:
CroneKorkN 2026-01-18 18:16:01 +01:00
parent 17fca8fae5
commit 7ade38ecad
Signed by: cronekorkn
SSH key fingerprint: SHA256:v0410ZKfuO1QHdgKBsdQNF64xmTxOF8osF1LIqwTcVw
12 changed files with 177 additions and 23 deletions

View file

@ -1,14 +1,19 @@
class OverlaysController < ApplicationController
before_action :set_overlay, only: [ :destroy ]
before_action :set_server_template, only: [ :create ]
before_action :set_server_template, only: [ :create, :destroy ]
def index
@system_overlays = Overlay.system_overlays
@custom_overlays = current_user.overlays.custom_overlays.order(:name)
end
def new
@overlay = current_user.overlays.build
end
def create
@server_template = current_user.server_templates.find(params[:server_template_id])
if @server_template.present?
# Attach existing overlay to a template
@overlay = Overlay.find(params[:overlay_id])
position = @server_template.template_overlays.maximum(:position).to_i + 1
@ -20,27 +25,53 @@ class OverlaysController < ApplicationController
else
redirect_to @server_template, alert: "Failed to add overlay"
end
else
# 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
def destroy
authorize_user!
server_template = @overlay.server_templates.find(params[:server_template_id])
@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 })
redirect_to server_template, notice: "Overlay removed successfully!"
if @server_template.present?
authorize_user_for_template_overlay!
@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 })
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
private
def set_overlay
@overlay = Overlay.find(params[:id])
@overlay = Overlay.find(params[:id]) if params[:id].present?
end
def set_server_template
return unless params[:server_template_id].present?
@server_template = current_user.server_templates.find(params[:server_template_id])
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
end
def authorize_owner!
redirect_to overlays_path, alert: "Not authorized" unless @overlay.user_id == current_user.id
end
end

View file

@ -1,6 +1,6 @@
class ConfigOption < ApplicationRecord
belongs_to :server_template
validates :config_key, :config_value, presence: true
validates :config_key, presence: true
validates :config_key, uniqueness: { scope: :server_template_id }
end

View file

@ -3,11 +3,37 @@ class Overlay < ApplicationRecord
has_many :template_overlays, dependent: :destroy
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 :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 :custom_overlays, -> { where(overlay_type: "custom") }
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

View file

@ -1,6 +1,6 @@
class StartupParam < ApplicationRecord
belongs_to :server_template
validates :param_key, :param_value, presence: true
validates :param_key, presence: true
validates :param_key, uniqueness: { scope: :server_template_id }
end

View 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>

View 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.

View 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"

View file

@ -18,6 +18,8 @@
- @server_template.template_overlays.ordered.each do |to|
li
= 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"
- else
p No overlays selected.

View file

@ -17,7 +17,7 @@ Rails.application.routes.draw do
resources :startup_params, only: [ :create, :destroy ]
end
resources :overlays, only: [ :index ]
resources :overlays, only: [ :index, :new, :create, :destroy ]
resources :servers do
member do

View 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
View file

@ -10,7 +10,7 @@
#
# 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|
t.string "action", 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.string "name", 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.integer "user_id"
t.index ["user_id", "name"], name: "index_overlays_on_user_id_and_name", unique: true

View file

@ -46,7 +46,7 @@ module L4dServer
def mount_overlayfs
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"
mount_cmd = "fuse-overlayfs -o lowerdir=#{lower_dirs},upperdir=#{server_dir}/upper,workdir=#{server_dir}/work #{server_dir}/merged"