deployed 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ [x-cloak] { display: none !important; }
6
+
7
+ /* Style for scrollable-div to fill the available vertical space */
8
+ #scrollable-div {
9
+ flex: 1; /* Grow to fill available vertical space */
10
+ }
11
+
12
+ #resize-handle {
13
+ cursor: ns-resize; /* Change cursor to indicate vertical resizing */
14
+ }
15
+
16
+ /* Style for deploy-output-container */
17
+ #deploy-output-container {
18
+ min-height: 150px;
19
+ }
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deployed
4
+ class ApplicationController < ActionController::Base
5
+ layout 'deployed/application'
6
+
7
+ helper Deployed::Engine.helpers
8
+
9
+ before_action :initialize_deployed
10
+
11
+ private
12
+
13
+ # A bunch of housekeeping stuff to make things run
14
+ def initialize_deployed
15
+ Deployed.setup!
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deployed
4
+ # Provides information on the current ./config/deploy.yml
5
+ class ConfigController < ApplicationController
6
+ def show
7
+ respond_to(&:html)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deployed
4
+ # Provides a git status check to see if we are deploying uncommitted changes
5
+ class GitController < ApplicationController
6
+ def uncommitted_check
7
+ @git_status = `git status --porcelain`
8
+ respond_to(&:html)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deployed
4
+ # Provides a centralized way to run all `kamal [command]` executions and streams to the browser
5
+ class RunController < ApplicationController
6
+ class ConcurrentProcessRunning < StandardError; end
7
+
8
+ include ActionController::Live
9
+
10
+ before_action :set_headers
11
+
12
+ # Endpoint to execute the kamal command
13
+ def execute
14
+ if process_running?
15
+ raise(ConcurrentProcessRunning)
16
+ elsif stored_pid
17
+ release_process
18
+ end
19
+
20
+ sse.write(
21
+ "<div class='text-slate-400'>> <span class='text-slate-300 font-semibold'>kamal #{command}</span></div>",
22
+ event: 'message'
23
+ )
24
+
25
+ read_io, write_io = IO.pipe
26
+
27
+ # Fork a child process
28
+ Deployed::CurrentExecution.child_pid = fork do
29
+ # Redirect the standard output to the write end of the pipe
30
+ $stdout.reopen(write_io)
31
+
32
+ # Execute the command
33
+ exec("kamal #{command}; echo \"[Kamal Rails] End transmission\"")
34
+ end
35
+
36
+ lock_process
37
+
38
+ sse.write(
39
+ "<div class='text-slate-400' data-child-pid=\"#{Deployed::CurrentExecution.child_pid}\"></div>",
40
+ event: 'message'
41
+ )
42
+
43
+ write_io.close
44
+
45
+ # Use a separate thread to read and stream the output
46
+ output_thread = Thread.new do
47
+ read_io.each_line do |line|
48
+ output = line.strip
49
+ output = output.gsub('49.13.91.176', '[redacted]')
50
+ text_color_class = 'text-green-400'
51
+
52
+ # Hackish way of dealing with error messages at the moment
53
+ if output.include?('[31m')
54
+ text_color_class = 'text-red-500'
55
+ output.gsub!('[31m', '')
56
+ output.gsub!('[0m', '')
57
+ end
58
+
59
+ sse.write("<div class='#{text_color_class}'>#{output}</div>", event: 'message')
60
+ end
61
+
62
+ # Ensure the response stream and the thread are closed properly
63
+ sse.close
64
+ response.stream.close
65
+ end
66
+
67
+ # Ensure that the thread is joined when the execution is complete
68
+ Process.wait
69
+ output_thread.join
70
+ rescue ConcurrentProcessRunning
71
+ sse.write(
72
+ "<div class='text-red-500'>Existing process running with PID: #{stored_pid}</div>",
73
+ event: 'message'
74
+ )
75
+ logger.info 'Existing process running'
76
+ rescue ActionController::Live::ClientDisconnected
77
+ logger.info 'Client Disconnected'
78
+ rescue IOError
79
+ logger.info 'IOError'
80
+ ensure
81
+ sse.close
82
+ response.stream.close
83
+ release_process
84
+ end
85
+
86
+ # Endpoint to cancel currently running process
87
+ def cancel
88
+ if process_running?
89
+ # If a process is running, get the PID and attempt to kill it
90
+ begin
91
+ Process.kill('TERM', stored_pid)
92
+ sse.write(
93
+ "<div class='text-yellow-400'>Cancelled the process with PID: #{stored_pid}</div>",
94
+ event: 'message'
95
+ )
96
+ release_process
97
+ rescue Errno::ESRCH
98
+ sse.write(
99
+ "<div class='text-red-500'>Process with PID #{stored_pid} is not running.</div>",
100
+ event: 'message'
101
+ )
102
+ end
103
+ else
104
+ sse.write(
105
+ "<div class='text-slate-400'>No process is currently running, nothing to cancel.</div>",
106
+ event: 'message'
107
+ )
108
+ end
109
+ rescue ActionController::Live::ClientDisconnected
110
+ logger.info 'Client Disconnected'
111
+ rescue IOError
112
+ logger.info 'IOError'
113
+ ensure
114
+ sse.write(
115
+ '[Kamal Rails] End transmission',
116
+ event: 'message'
117
+ )
118
+ sse.close
119
+ response.stream.close
120
+ release_process
121
+ end
122
+
123
+ private
124
+
125
+ def set_headers
126
+ response.headers['Content-Type'] = 'text/event-stream'
127
+ response.headers['Last-Modified'] = Time.now.httpdate
128
+ end
129
+
130
+ def sse
131
+ @sse ||= SSE.new(response.stream, event: 'Stream Started')
132
+ end
133
+
134
+ def command
135
+ params[:command]
136
+ end
137
+
138
+ def lock_file_path
139
+ Rails.root.join(Deployed::DIRECTORY, 'process.lock')
140
+ end
141
+
142
+ def lock_process
143
+ File.open(lock_file_path, 'a') do |file|
144
+ file.puts(Deployed::CurrentExecution.child_pid)
145
+ end
146
+ end
147
+
148
+ def release_process
149
+ return unless File.exist?(lock_file_path)
150
+
151
+ File.delete(lock_file_path)
152
+ end
153
+
154
+ def stored_pid
155
+ return false unless File.exist?(lock_file_path)
156
+
157
+ value = File.read(lock_file_path).to_i
158
+
159
+ if value.is_a?(Integer)
160
+ value
161
+ else
162
+ false
163
+ end
164
+ end
165
+
166
+ def process_running?
167
+ return false unless stored_pid
168
+
169
+ begin
170
+ # Send signal 0 to the process to check if it exists
171
+ Process.kill(0, stored_pid)
172
+ true
173
+ rescue Errno::ESRCH
174
+ false
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deployed
4
+ # Provides a way to setup ./config/deploy.yml
5
+ class SetupController < ApplicationController
6
+ def new
7
+ respond_to do |format|
8
+ format.html
9
+ end
10
+ end
11
+
12
+ def create
13
+ `kamal init`
14
+
15
+ respond_to do |format|
16
+ format.html { redirect_to root_path }
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deployed
4
+ # Provides the main entry point for the app
5
+ class WelcomeController < ApplicationController
6
+ def index
7
+ respond_to(&:html)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deployed
4
+ module ApplicationHelper
5
+ def rocket_icon
6
+ output = <<~ICON
7
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
8
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15.59 14.37a6 6 0 01-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 006.16-12.12A14.98 14.98 0 009.631 8.41m5.96 5.96a14.926 14.926 0 01-5.841 2.58m-.119-8.54a6 6 0 00-7.381 5.84h4.8m2.581-5.84a14.927 14.927 0 00-2.58 5.84m2.699 2.7c-.103.021-.207.041-.311.06a15.09 15.09 0 01-2.448-2.448 14.9 14.9 0 01.06-.312m-2.24 2.39a4.493 4.493 0 00-1.757 4.306 4.493 4.493 0 004.306-1.758M16.5 9a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0z" />
9
+ </svg>
10
+ ICON
11
+
12
+ output.html_safe
13
+ end
14
+
15
+ def config_icon
16
+ output = <<~ICON
17
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
18
+ <path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75" />
19
+ </svg>
20
+ ICON
21
+
22
+ output.html_safe
23
+ end
24
+
25
+ def tools_icon
26
+ output = <<~ICON
27
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
28
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75a4.5 4.5 0 01-4.884 4.484c-1.076-.091-2.264.071-2.95.904l-7.152 8.684a2.548 2.548 0 11-3.586-3.586l8.684-7.152c.833-.686.995-1.874.904-2.95a4.5 4.5 0 016.336-4.486l-3.276 3.276a3.004 3.004 0 002.25 2.25l3.276-3.276c.256.565.398 1.192.398 1.852z" />
29
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.867 19.125h.008v.008h-.008v-.008z" />
30
+ </svg>
31
+ ICON
32
+
33
+ output.html_safe
34
+ end
35
+
36
+ def right_arrow
37
+ output = <<~ICON
38
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
39
+ <path stroke-linecap="round" stroke-linejoin="round" d="M17.25 8.25L21 12m0 0l-3.75 3.75M21 12H3" />
40
+ </svg>
41
+ ICON
42
+
43
+ output.html_safe
44
+ end
45
+
46
+ def kamal_exec_button(label:, command:)
47
+ button_tag(
48
+ label,
49
+ type: 'button',
50
+ onclick: "execKamal('#{command}')",
51
+ class: 'rounded-md bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50',
52
+ 'x-bind:disabled' => '$store.process.running',
53
+ 'x-bind:class' => "{'opacity-50': $store.process.running}",
54
+ 'x-data' => ''
55
+ )
56
+ end
57
+
58
+ def kamal_abort_button
59
+ button_tag(
60
+ 'Abort',
61
+ type: 'button',
62
+ onclick: "abortKamal()",
63
+ class: 'rounded-md bg-red-600 px-3 py-1.5 text-sm text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600',
64
+ 'x-bind:disabled' => '$store.process.abortInProgress',
65
+ 'x-bind:class' => "{'opacity-50': $store.process.abortInProgress}",
66
+ 'x-text' => "$store.process.abortInProgress ? 'Aborting' : 'Abort'",
67
+ 'x-data' => ''
68
+ )
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deployed
4
+ module LogOutputHelper
5
+ def deploy_output_intro
6
+ output = <<~HTML
7
+ <div>Ready... Set... Deploy!</div>
8
+ HTML
9
+
10
+ output.html_safe
11
+ end
12
+
13
+ def deploy_output_kamal_version
14
+ output = <<~HTML
15
+ <div>
16
+ Using <span class="text-slate-300">kamal #{::Kamal::VERSION}</span>
17
+ </div>
18
+ HTML
19
+
20
+ output.html_safe
21
+ end
22
+
23
+ def deploy_output_missing_config
24
+ return unless Deployed::Config.requires_init
25
+
26
+ output = <<~HTML
27
+ <div class="text-red-500">WARNING: ./config/deploy.yml file not detected</div>
28
+ HTML
29
+
30
+ output.html_safe
31
+ end
32
+
33
+ def deploy_output_spinner
34
+ output = <<~HTML
35
+ <div id="spinner" class="hidden mt-2 pb-4">
36
+ <svg class="animate-spin -ml-1 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
37
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
38
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
39
+ </svg>
40
+ </div>
41
+ HTML
42
+
43
+ output.html_safe
44
+ end
45
+
46
+ def deploy_output_resize_handler
47
+ output = <<~ICON
48
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
49
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM12.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM18.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" />
50
+ </svg>
51
+ ICON
52
+
53
+ output.html_safe
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Deployed
6
+ # Provides a model to track the current values in ./config/deploy.yml
7
+ class Config < ActiveSupport::CurrentAttributes
8
+ attribute :service, :image, :servers, :ssh, :volumes, :registry, :env, :traefik, :requires_init, :env_values
9
+
10
+ def self.init!(yaml_file = './config/deploy.yml', dot_env_file = '.env')
11
+ self.requires_init = File.exist?(yaml_file) ? false : true
12
+
13
+ unless requires_init
14
+ yaml_data = YAML.load_file(yaml_file)
15
+ self.service = yaml_data['service']
16
+ self.image = yaml_data['image']
17
+ self.servers = yaml_data['servers']
18
+ self.ssh = yaml_data['ssh']
19
+ self.volumes = yaml_data['volumes']
20
+ self.registry = yaml_data['registry']
21
+ self.env = yaml_data['env'] || {}
22
+ self.traefik = yaml_data['traefik']
23
+ end
24
+
25
+ if File.exist?(dot_env_file)
26
+ self.env_values = {}
27
+
28
+ File.open(dot_env_file, 'r') do |file|
29
+ file.each_line do |line|
30
+ key, value = line.strip.split('=')
31
+ self.env_values[key] = value
32
+ end
33
+ end
34
+ end
35
+
36
+ requires_init
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deployed
4
+ # Provides a way to track the current child_pid
5
+ class CurrentExecution < ActiveSupport::CurrentAttributes
6
+ attribute :child_pid
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ <%= turbo_frame_tag 'git-status' do %>
2
+ <% unless @git_status.empty? %>
3
+ <div class="mt-4 text-sm">You have uncommitted files!</div>
4
+ <% end %>
5
+ <% end %>
@@ -0,0 +1,47 @@
1
+ <%= turbo_frame_tag 'deployed-init', target: '_top' do %>
2
+ <div class="relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true" x-data x-show="!$store.ready">
3
+ <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
4
+ x-data
5
+ x-show="!$store.ready"
6
+ x-transition:enter="ease-out duration-300"
7
+ x-transition:enter-start="opacity-0"
8
+ x-transition:enter-end="opacity-100"
9
+ x-transition:leave="ease-in duration-200"
10
+ x-transition:leave-start="opacity-100"
11
+ x-transition:leave-end="opacity-0"></div>
12
+
13
+ <div class="fixed inset-0 z-10 w-screen overflow-y-auto">
14
+ <div class="flex min-h-full items-center justify-center p-4 text-center">
15
+ <div class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all my-8 w-full max-w-lg p-6"
16
+ x-data
17
+ x-show="!$store.ready"
18
+ x-transition:enter="ease-out duration-300"
19
+ x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
20
+ x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
21
+ x-transition:leave="ease-in duration-200"
22
+ x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
23
+ x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
24
+ <div>
25
+ <div class="mt-5">
26
+ <div class="flex justify-center">
27
+ <h3 class="font-semibold mb-5 flex items-center text-xl">
28
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mr-2 text-red-500">
29
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
30
+ </svg>
31
+ Kamal Initialization Required
32
+ </h3>
33
+ </div>
34
+ <div class="text-gray-500 space-y-4">
35
+ <p><span class="font-mono bg-slate-700 px-2 py-0.5 rounded-md text-slate-300">./config/deploy.yml</span> not detected.</p>
36
+ <p>We will run <code class="font-mono bg-slate-700 px-2 py-0.5 rounded-md text-slate-300">kamal init</code> for you.</p>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ <div class="mt-5">
41
+ <%= button_to 'Setup Kamal', setup_path, method: :post, class: 'inline-flex w-full justify-center rounded-full bg-sky-600 py-3 font-semibold text-white shadow-sm hover:bg-sky-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-600 sm:col-start-2' %>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ <% end %>
@@ -0,0 +1,128 @@
1
+ <div class="space-y-10 divide-y divide-gray-900/10">
2
+ <div class="grid grid-cols-1 gap-x-8 gap-y-4 md:grid-cols-3">
3
+ <div class="px-0">
4
+ <h2 class="text-base font-semibold leading-7 text-gray-900 flex items-center">
5
+ <span class="mr-2"><%= rocket_icon %></span>
6
+ Deployment
7
+ </h2>
8
+ </div>
9
+
10
+ <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-xl md:col-span-2">
11
+ <div class="px-4 py-6 sm:p-8 flex flex-col space-y-4">
12
+ <div class="divide-y divide-gray-100">
13
+ <div class="!pt-0 py-4">
14
+ <%= kamal_exec_button(label: 'Deploy', command: 'deploy') %>
15
+ <!-- Asynchronously fetch git status -->
16
+ <%= turbo_frame_tag 'git-status', src: git_uncommitted_check_url %>
17
+ </div>
18
+ <div class="py-4">
19
+ <%= kamal_exec_button(label: 'App Details', command: 'app details') %>
20
+ <%= kamal_exec_button(label: 'App Containers', command: 'app containers') %>
21
+ </div>
22
+ <div class="!pb-0 py-4">
23
+ <%= kamal_exec_button(label: 'App Boot', command: 'app boot') %>
24
+ <%= kamal_exec_button(label: 'App Start', command: 'app start') %>
25
+ <%= kamal_exec_button(label: 'App Stop', command: 'app stop') %>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ </div>
31
+
32
+ <div class="grid grid-cols-1 gap-x-8 gap-y-4 pt-10 md:grid-cols-3">
33
+ <div class="px-0">
34
+ <h2 class="text-base font-semibold leading-7 text-gray-900 flex items-center">
35
+ <span class="mr-2"><%= config_icon %></span>
36
+ Configuration
37
+ </h2>
38
+ </div>
39
+
40
+ <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-xl md:col-span-2">
41
+ <div class="px-4 py-6 sm:p-8 flex flex-col space-y-4">
42
+ <% if Deployed::Config.requires_init %>
43
+ <p class="text-slate-700">Your <span class="font-mono bg-slate-700 px-2 py-0.5 rounded-md text-slate-300">./config/deploy.yml</span> file does not exist. You will need to create it.</p>
44
+ <% else %>
45
+ <dl class="divide-y divide-gray-100">
46
+ <div class="!pt-0 py-3">
47
+ <dt class="text-sm font-medium text-gray-900">App</dt>
48
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
49
+ <span class="font-mono"><%= Deployed::Config.service %></span>
50
+ </dd>
51
+ </div>
52
+ <div class="py-3">
53
+ <dt class="text-sm font-medium text-gray-900">Image</dt>
54
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
55
+ <span class="font-mono"><%= Deployed::Config.image %></span>
56
+ </dd>
57
+ </div>
58
+ <div class="py-3">
59
+ <dt class="text-sm font-medium text-gray-900">Environment variables</dt>
60
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
61
+ <% if !Deployed::Config.env.dig('secret').present? && !Deployed::Config.env.dig('clear').present? %>
62
+ <p>You have not specified any environment variables in your configuration file. This is likely an error. Please check your <span class="font-mono font-medium">./config/deploy.yml</span> file.</p>
63
+ <% end %>
64
+
65
+ <% if Deployed::Config.env.dig('secret').present? %>
66
+ <div class="italic mt-4">Secret</div>
67
+ <ul>
68
+ <% Deployed::Config.env.dig('secret').each do |secret_env| %>
69
+ <li class="flex space-x-1">
70
+ <span class="font-mono"><%= secret_env %></span>
71
+ <span><%= right_arrow %></span>
72
+ <% if Deployed::Config.env_values[secret_env].present? %>
73
+ <span class="font-mono text-slate-400">
74
+ <%= redacted_string = '*' * (Deployed::Config.env_values[secret_env].length - 6) + Deployed::Config.env_values[secret_env][-6..-1] %>
75
+ </span>
76
+ <% else %>
77
+ <span class="text-slate-400">Not found</span>
78
+ <% end %>
79
+ </li>
80
+ <% end %>
81
+ </ul>
82
+ <% end %>
83
+
84
+ <% if Deployed::Config.env['clear'].present? %>
85
+ <div class="italic mt-4">Secret</div>
86
+ <ul>
87
+ <% Deployed::Config.env['clear'].each do |secret_env| %>
88
+ <li class="flex space-x-1">
89
+ <span class="font-mono"><%= secret_env %></span>
90
+ <span><%= right_arrow %></span>
91
+ <% if Deployed::Config.env_values[secret_env].present? %>
92
+ <span class="font-mono text-slate-400">
93
+ <%= Deployed::Config.env_values[secret_env] %>
94
+ </span>
95
+ <% else %>
96
+ <span class="text-slate-400">Not found</span>
97
+ <% end %>
98
+ </li>
99
+ <% end %>
100
+ </ul>
101
+ <% end %>
102
+ </dd>
103
+ </div>
104
+ </dl>
105
+ <% end %>
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ <div class="grid grid-cols-1 gap-x-8 gap-y-4 pt-10 md:grid-cols-3">
111
+ <div class="px-0">
112
+ <h2 class="text-base font-semibold leading-7 text-gray-900 flex items-center">
113
+ <span class="mr-2"><%= tools_icon %></span>
114
+ Utilities
115
+ </h2>
116
+ </div>
117
+
118
+ <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-xl md:col-span-2">
119
+ <div class="px-4 py-6 sm:p-8 flex flex-col space-y-4">
120
+ <div><%= kamal_exec_button(label: 'Perform Healthcheck', command: 'healthcheck --verbose') %></div>
121
+ <div>
122
+ <%= kamal_exec_button(label: 'Check Lock Status', command: 'lock status') %>
123
+ <%= kamal_exec_button(label: 'Release Lock', command: 'lock release') %>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </div>