summaryrefslogtreecommitdiffstats
path: root/cmd/deploy/templates
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-01-23 20:54:46 -0800
committerbndw <ben@bdw.to>2026-01-23 20:54:46 -0800
commit98b9af372025595e8a4255538e2836e019311474 (patch)
tree0a26fa5a8a19ea8565da6d63e1f19c21fc170d12 /cmd/deploy/templates
parent7fcb9dfa87310e91b527829ece9989decb6fda64 (diff)
Add deploy command and fix static site naming
Static sites now default to using the domain as the name instead of the source directory basename, preventing conflicts when multiple sites use the same directory name (e.g., dist). Also fixes .gitignore to not exclude cmd/deploy/ directory.
Diffstat (limited to 'cmd/deploy/templates')
-rw-r--r--cmd/deploy/templates/webui.html440
1 files changed, 440 insertions, 0 deletions
diff --git a/cmd/deploy/templates/webui.html b/cmd/deploy/templates/webui.html
new file mode 100644
index 0000000..052d599
--- /dev/null
+++ b/cmd/deploy/templates/webui.html
@@ -0,0 +1,440 @@
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Deploy - Web UI</title>
7 <style>
8 * {
9 margin: 0;
10 padding: 0;
11 box-sizing: border-box;
12 }
13
14 body {
15 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
16 background: #f5f5f5;
17 color: #333;
18 line-height: 1.6;
19 }
20
21 header {
22 background: #2c3e50;
23 color: white;
24 padding: 1.5rem 2rem;
25 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
26 }
27
28 header h1 {
29 font-size: 1.8rem;
30 font-weight: 600;
31 }
32
33 header p {
34 color: #bdc3c7;
35 margin-top: 0.25rem;
36 font-size: 0.9rem;
37 }
38
39 .container {
40 max-width: 1200px;
41 margin: 2rem auto;
42 padding: 0 2rem;
43 }
44
45 .empty-state {
46 text-align: center;
47 padding: 4rem 2rem;
48 background: white;
49 border-radius: 8px;
50 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
51 }
52
53 .empty-state h2 {
54 color: #7f8c8d;
55 font-weight: 500;
56 margin-bottom: 0.5rem;
57 }
58
59 .empty-state p {
60 color: #95a5a6;
61 }
62
63 .host-section {
64 margin-bottom: 2rem;
65 }
66
67 .host-header {
68 background: white;
69 padding: 1rem 1.5rem;
70 border-radius: 8px 8px 0 0;
71 border-left: 4px solid #3498db;
72 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
73 }
74
75 .host-header h2 {
76 font-size: 1.3rem;
77 color: #2c3e50;
78 font-weight: 600;
79 }
80
81 .apps-grid {
82 display: grid;
83 grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
84 gap: 1rem;
85 padding: 1rem;
86 background: #ecf0f1;
87 border-radius: 0 0 8px 8px;
88 }
89
90 .app-card {
91 background: white;
92 padding: 1.5rem;
93 border-radius: 6px;
94 box-shadow: 0 1px 3px rgba(0,0,0,0.1);
95 transition: transform 0.2s, box-shadow 0.2s;
96 }
97
98 .app-card:hover {
99 transform: translateY(-2px);
100 box-shadow: 0 4px 8px rgba(0,0,0,0.15);
101 }
102
103 .app-header {
104 display: flex;
105 justify-content: space-between;
106 align-items: center;
107 margin-bottom: 1rem;
108 }
109
110 .app-name {
111 font-size: 1.2rem;
112 font-weight: 600;
113 color: #2c3e50;
114 }
115
116 .app-type {
117 padding: 0.25rem 0.75rem;
118 border-radius: 12px;
119 font-size: 0.75rem;
120 font-weight: 500;
121 text-transform: uppercase;
122 }
123
124 .app-type.app {
125 background: #3498db;
126 color: white;
127 }
128
129 .app-type.static {
130 background: #2ecc71;
131 color: white;
132 }
133
134 .app-info {
135 margin-bottom: 0.5rem;
136 }
137
138 .app-info-label {
139 color: #7f8c8d;
140 font-size: 0.85rem;
141 font-weight: 500;
142 margin-bottom: 0.25rem;
143 }
144
145 .app-info-value {
146 color: #2c3e50;
147 font-family: 'Monaco', 'Courier New', monospace;
148 font-size: 0.9rem;
149 word-break: break-all;
150 }
151
152 .app-info-value a {
153 color: #3498db;
154 text-decoration: none;
155 }
156
157 .app-info-value a:hover {
158 text-decoration: underline;
159 }
160
161 .config-buttons {
162 margin-top: 1rem;
163 padding-top: 1rem;
164 border-top: 1px solid #ecf0f1;
165 display: flex;
166 gap: 0.5rem;
167 flex-wrap: wrap;
168 }
169
170 .config-btn {
171 padding: 0.4rem 0.8rem;
172 background: #3498db;
173 color: white;
174 border: none;
175 border-radius: 4px;
176 font-size: 0.8rem;
177 cursor: pointer;
178 transition: background 0.2s;
179 }
180
181 .config-btn:hover {
182 background: #2980b9;
183 }
184
185 .config-btn.secondary {
186 background: #95a5a6;
187 }
188
189 .config-btn.secondary:hover {
190 background: #7f8c8d;
191 }
192
193 .modal {
194 display: none;
195 position: fixed;
196 z-index: 1000;
197 left: 0;
198 top: 0;
199 width: 100%;
200 height: 100%;
201 overflow: auto;
202 background-color: rgba(0,0,0,0.6);
203 }
204
205 .modal.active {
206 display: block;
207 }
208
209 .modal-content {
210 background-color: #fefefe;
211 margin: 5% auto;
212 padding: 0;
213 border-radius: 8px;
214 width: 90%;
215 max-width: 900px;
216 max-height: 80vh;
217 display: flex;
218 flex-direction: column;
219 box-shadow: 0 4px 20px rgba(0,0,0,0.3);
220 }
221
222 .modal-header {
223 padding: 1.5rem;
224 border-bottom: 1px solid #ecf0f1;
225 display: flex;
226 justify-content: space-between;
227 align-items: center;
228 }
229
230 .modal-header h3 {
231 margin: 0;
232 color: #2c3e50;
233 }
234
235 .modal-path {
236 font-family: 'Monaco', 'Courier New', monospace;
237 font-size: 0.85rem;
238 color: #7f8c8d;
239 margin-top: 0.25rem;
240 }
241
242 .close {
243 color: #aaa;
244 font-size: 28px;
245 font-weight: bold;
246 cursor: pointer;
247 line-height: 1;
248 }
249
250 .close:hover {
251 color: #000;
252 }
253
254 .modal-body {
255 padding: 1.5rem;
256 overflow: auto;
257 flex: 1;
258 }
259
260 .config-content {
261 background: #282c34;
262 color: #abb2bf;
263 padding: 1rem;
264 border-radius: 4px;
265 font-family: 'Monaco', 'Courier New', monospace;
266 font-size: 0.85rem;
267 line-height: 1.5;
268 white-space: pre-wrap;
269 word-wrap: break-word;
270 overflow-x: auto;
271 text-align: left;
272 }
273
274 .loading {
275 text-align: center;
276 padding: 2rem;
277 color: #7f8c8d;
278 }
279
280 .refresh-info {
281 text-align: center;
282 color: #7f8c8d;
283 font-size: 0.9rem;
284 margin-top: 2rem;
285 padding: 1rem;
286 }
287 </style>
288</head>
289<body>
290 <header>
291 <h1>Deploy Web UI</h1>
292 <p>Manage your VPS deployments</p>
293 </header>
294
295 <div class="container">
296 {{if not .Hosts}}
297 <div class="empty-state">
298 <h2>No deployments found</h2>
299 <p>Use the CLI to deploy your first app or static site</p>
300 </div>
301 {{else}}
302 {{range .Hosts}}
303 <div class="host-section">
304 <div class="host-header">
305 <h2>{{.Host}}</h2>
306 </div>
307 <div class="apps-grid">
308 {{range .Apps}}
309 <div class="app-card">
310 <div class="app-header">
311 <div class="app-name">{{.Name}}</div>
312 <div class="app-type {{.Type}}">{{.Type}}</div>
313 </div>
314
315 <div class="app-info">
316 <div class="app-info-label">Domain</div>
317 <div class="app-info-value">
318 <a href="https://{{.Domain}}" target="_blank">{{.Domain}}</a>
319 </div>
320 </div>
321
322 {{if eq .Type "app"}}
323 <div class="app-info">
324 <div class="app-info-label">Port</div>
325 <div class="app-info-value">{{.Port}}</div>
326 </div>
327 {{end}}
328
329 <div class="config-buttons">
330 {{if eq .Type "app"}}
331 <button class="config-btn" onclick="showConfig('{{.Host}}', '{{.Name}}', 'systemd')">Systemd Unit</button>
332 {{end}}
333 <button class="config-btn" onclick="showConfig('{{.Host}}', '{{.Name}}', 'caddy')">Caddy Config</button>
334 {{if .Env}}
335 <button class="config-btn secondary" onclick="showConfig('{{.Host}}', '{{.Name}}', 'env')">Environment</button>
336 {{end}}
337 </div>
338 </div>
339 {{end}}
340 </div>
341 </div>
342 {{end}}
343 {{end}}
344
345 <div class="refresh-info">
346 Refresh the page to see latest changes
347 </div>
348 </div>
349
350 <!-- Modal -->
351 <div id="configModal" class="modal">
352 <div class="modal-content">
353 <div class="modal-header">
354 <div>
355 <h3 id="modalTitle">Configuration</h3>
356 <div class="modal-path" id="modalPath"></div>
357 </div>
358 <span class="close" onclick="closeModal()">&times;</span>
359 </div>
360 <div class="modal-body">
361 <div id="modalContent" class="loading">Loading...</div>
362 </div>
363 </div>
364 </div>
365
366 <script>
367 const modal = document.getElementById('configModal');
368 const modalTitle = document.getElementById('modalTitle');
369 const modalPath = document.getElementById('modalPath');
370 const modalContent = document.getElementById('modalContent');
371
372 function closeModal() {
373 modal.classList.remove('active');
374 }
375
376 window.onclick = function(event) {
377 if (event.target == modal) {
378 closeModal();
379 }
380 }
381
382 async function showConfig(host, app, type) {
383 modal.classList.add('active');
384 modalContent.innerHTML = '<div class="loading">Loading...</div>';
385
386 const titles = {
387 'systemd': 'Systemd Service Unit',
388 'caddy': 'Caddy Configuration',
389 'env': 'Environment Variables'
390 };
391
392 modalTitle.textContent = titles[type];
393
394 try {
395 const response = await fetch(`/api/configs?host=${encodeURIComponent(host)}&app=${encodeURIComponent(app)}`);
396 if (!response.ok) {
397 throw new Error(`HTTP error! status: ${response.status}`);
398 }
399 const configs = await response.json();
400
401 let content = '';
402 let path = '';
403
404 switch(type) {
405 case 'systemd':
406 content = configs.systemd || 'No systemd config available';
407 path = configs.systemdPath || '';
408 break;
409 case 'caddy':
410 content = configs.caddy || 'No Caddy config available';
411 path = configs.caddyPath || '';
412 break;
413 case 'env':
414 content = configs.env || 'No environment variables';
415 path = configs.envPath || '';
416 break;
417 }
418
419 modalPath.textContent = path;
420 modalContent.innerHTML = `<div class="config-content">${escapeHtml(content)}</div>`;
421 } catch (error) {
422 modalContent.innerHTML = `<div class="loading">Error loading config: ${error.message}</div>`;
423 }
424 }
425
426 function escapeHtml(text) {
427 const div = document.createElement('div');
428 div.textContent = text;
429 return div.innerHTML;
430 }
431
432 // Close modal with Escape key
433 document.addEventListener('keydown', function(event) {
434 if (event.key === 'Escape') {
435 closeModal();
436 }
437 });
438 </script>
439</body>
440</html>