diff options
| author | bndw <ben@bdw.to> | 2026-01-24 09:48:34 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-01-24 09:48:34 -0800 |
| commit | 5861e465a2ccf31d87ea25ac145770786f9cc96e (patch) | |
| tree | 4ac6b57a06b46d8492717b235909f9e0db0b4f22 /cmd/deploy/templates/webui.html | |
| parent | ef37850c7090493cf2b26d2e565511fe23cc9bfc (diff) | |
Rename project from deploy to ship
- Rename module to github.com/bdw/ship
- Rename cmd/deploy to cmd/ship
- Update all import paths
- Update config path from ~/.config/deploy to ~/.config/ship
- Update VPS env path from /etc/deploy to /etc/ship
- Update README, Makefile, and docs
Diffstat (limited to 'cmd/deploy/templates/webui.html')
| -rw-r--r-- | cmd/deploy/templates/webui.html | 440 |
1 files changed, 0 insertions, 440 deletions
diff --git a/cmd/deploy/templates/webui.html b/cmd/deploy/templates/webui.html deleted file mode 100644 index 052d599..0000000 --- a/cmd/deploy/templates/webui.html +++ /dev/null | |||
| @@ -1,440 +0,0 @@ | |||
| 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()">×</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> | ||
