manage overlays
This commit is contained in:
parent
17fca8fae5
commit
7ade38ecad
12 changed files with 177 additions and 23 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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|
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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.
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue