upaas/templates/app_detail.html
sneak 5fb0b111fc Use ULID for app IDs and Docker label for container lookup
- Replace UUID with ULID for app ID generation (lexicographically sortable)
- Remove container_id column from apps table (migration 002)
- Add upaas.id Docker label to identify containers by app ID
- Implement FindContainerByAppID in Docker client to query by label
- Update handlers and deploy service to use label-based container lookup
- Show system-managed upaas.id label in UI with editing disabled

Container association is now determined dynamically via Docker label
rather than stored in the database, making the system more resilient
to container recreation or external changes.
2025-12-29 16:06:40 +07:00

272 lines
12 KiB
HTML

{{template "base" .}}
{{define "title"}}{{.App.Name}} - upaas{{end}}
{{define "content"}}
{{template "nav" .}}
<main class="max-w-4xl mx-auto px-4 py-8">
<div class="mb-6">
<a href="/" class="text-primary-600 hover:text-primary-800 inline-flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
Back to Dashboard
</a>
</div>
{{template "alert-success" .}}
{{template "alert-error" .}}
<!-- Header -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
<div>
<div class="flex items-center gap-3">
<h1 class="text-2xl font-medium text-gray-900">{{.App.Name}}</h1>
{{if eq .App.Status "running"}}
<span class="badge-success">Running</span>
{{else if eq .App.Status "building"}}
<span class="badge-warning">Building</span>
{{else if eq .App.Status "error"}}
<span class="badge-error">Error</span>
{{else if eq .App.Status "stopped"}}
<span class="badge-neutral">Stopped</span>
{{else}}
<span class="badge-neutral">{{.App.Status}}</span>
{{end}}
</div>
<p class="text-gray-500 font-mono text-sm mt-1">{{.App.RepoURL}} @ {{.App.Branch}}</p>
</div>
<div class="flex gap-3">
<a href="/apps/{{.App.ID}}/edit" class="btn-secondary">Edit</a>
<form method="POST" action="/apps/{{.App.ID}}/deploy" class="inline">
<button type="submit" class="btn-success">Deploy Now</button>
</form>
</div>
</div>
<!-- Deploy Key -->
<div class="card p-6 mb-6">
<h2 class="section-title mb-4">Deploy Key</h2>
<p class="text-sm text-gray-500 mb-3">Add this SSH public key to your repository as a read-only deploy key:</p>
<div class="copy-field">
<code id="deploy-key" class="copy-field-value text-xs">{{.App.SSHPublicKey}}</code>
<button
type="button"
data-copy-target="deploy-key"
class="copy-btn"
title="Copy to clipboard"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
</button>
</div>
</div>
<!-- Webhook URL -->
<div class="card p-6 mb-6">
<h2 class="section-title mb-4">Webhook URL</h2>
<p class="text-sm text-gray-500 mb-3">Add this URL as a push webhook in your Gitea repository:</p>
<div class="copy-field">
<code id="webhook-url" class="copy-field-value text-xs">{{.WebhookURL}}</code>
<button
type="button"
data-copy-target="webhook-url"
class="copy-btn"
title="Copy to clipboard"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
</button>
</div>
</div>
<!-- Environment Variables -->
<div class="card p-6 mb-6">
<h2 class="section-title mb-4">Environment Variables</h2>
{{if .EnvVars}}
<div class="overflow-x-auto mb-4">
<table class="table">
<thead class="table-header">
<tr>
<th>Key</th>
<th>Value</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody class="table-body">
{{range .EnvVars}}
<tr>
<td class="font-mono font-medium">{{.Key}}</td>
<td class="font-mono text-gray-500">{{.Value}}</td>
<td class="text-right">
<form method="POST" action="/apps/{{$.App.ID}}/env/{{.ID}}/delete" class="inline" data-confirm="Delete this environment variable?">
<button type="submit" class="text-error-500 hover:text-error-700 text-sm">Delete</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
<form method="POST" action="/apps/{{.App.ID}}/env" class="flex flex-col sm:flex-row gap-2">
<input type="text" name="key" placeholder="KEY" required class="input flex-1 font-mono text-sm">
<input type="text" name="value" placeholder="value" required class="input flex-1 font-mono text-sm">
<button type="submit" class="btn-primary">Add</button>
</form>
</div>
<!-- Labels -->
<div class="card p-6 mb-6">
<h2 class="section-title mb-4">Docker Labels</h2>
<div class="overflow-x-auto mb-4">
<table class="table">
<thead class="table-header">
<tr>
<th>Key</th>
<th>Value</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody class="table-body">
<!-- System-managed upaas.id label -->
<tr class="bg-gray-50">
<td class="font-mono font-medium text-gray-600">upaas.id</td>
<td class="font-mono text-gray-500">{{.App.ID}}</td>
<td class="text-right">
<span class="text-xs text-gray-400">System</span>
</td>
</tr>
{{range .Labels}}
<tr>
<td class="font-mono font-medium">{{.Key}}</td>
<td class="font-mono text-gray-500">{{.Value}}</td>
<td class="text-right">
<form method="POST" action="/apps/{{$.App.ID}}/labels/{{.ID}}/delete" class="inline" data-confirm="Delete this label?">
<button type="submit" class="text-error-500 hover:text-error-700 text-sm">Delete</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<form method="POST" action="/apps/{{.App.ID}}/labels" class="flex flex-col sm:flex-row gap-2">
<input type="text" name="key" placeholder="label.key" required class="input flex-1 font-mono text-sm">
<input type="text" name="value" placeholder="value" required class="input flex-1 font-mono text-sm">
<button type="submit" class="btn-primary">Add</button>
</form>
</div>
<!-- Volumes -->
<div class="card p-6 mb-6">
<h2 class="section-title mb-4">Volume Mounts</h2>
{{if .Volumes}}
<div class="overflow-x-auto mb-4">
<table class="table">
<thead class="table-header">
<tr>
<th>Host Path</th>
<th>Container Path</th>
<th>Mode</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody class="table-body">
{{range .Volumes}}
<tr>
<td class="font-mono">{{.HostPath}}</td>
<td class="font-mono">{{.ContainerPath}}</td>
<td>
{{if .ReadOnly}}
<span class="badge-neutral">Read-only</span>
{{else}}
<span class="badge-info">Read-write</span>
{{end}}
</td>
<td class="text-right">
<form method="POST" action="/apps/{{$.App.ID}}/volumes/{{.ID}}/delete" class="inline" data-confirm="Delete this volume mount?">
<button type="submit" class="text-error-500 hover:text-error-700 text-sm">Delete</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
<form method="POST" action="/apps/{{.App.ID}}/volumes" class="flex flex-col sm:flex-row gap-2 items-end">
<div class="flex-1 w-full">
<input type="text" name="host_path" placeholder="/host/path" required class="input font-mono text-sm">
</div>
<div class="flex-1 w-full">
<input type="text" name="container_path" placeholder="/container/path" required class="input font-mono text-sm">
</div>
<label class="flex items-center gap-2 text-sm text-gray-600 whitespace-nowrap">
<input type="checkbox" name="readonly" value="1" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500">
Read-only
</label>
<button type="submit" class="btn-primary">Add</button>
</form>
</div>
<!-- Recent Deployments -->
<div class="card p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="section-title">Recent Deployments</h2>
<a href="/apps/{{.App.ID}}/deployments" class="text-primary-600 hover:text-primary-800 text-sm">View All</a>
</div>
{{if .Deployments}}
<div class="overflow-x-auto">
<table class="table">
<thead class="table-header">
<tr>
<th>Started</th>
<th>Status</th>
<th>Commit</th>
</tr>
</thead>
<tbody class="table-body">
{{range .Deployments}}
<tr>
<td class="text-gray-500">{{.StartedAt.Format "2006-01-02 15:04:05"}}</td>
<td>
{{if eq .Status "success"}}
<span class="badge-success">Success</span>
{{else if eq .Status "failed"}}
<span class="badge-error">Failed</span>
{{else if eq .Status "building"}}
<span class="badge-warning">Building</span>
{{else if eq .Status "deploying"}}
<span class="badge-info">Deploying</span>
{{else}}
<span class="badge-neutral">{{.Status}}</span>
{{end}}
</td>
<td class="font-mono text-gray-500 text-xs">
{{if .CommitSHA.Valid}}{{slice .CommitSHA.String 0 12}}{{else}}-{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p class="text-gray-500 text-sm">No deployments yet.</p>
{{end}}
</div>
<!-- Danger Zone -->
<div class="card border-2 border-error-500/20 bg-error-50/50 p-6">
<h2 class="text-lg font-medium text-error-700 mb-4">Danger Zone</h2>
<p class="text-error-600 text-sm mb-4">Deleting this app will remove all configuration and deployment history. This action cannot be undone.</p>
<form method="POST" action="/apps/{{.App.ID}}/delete" data-confirm="Are you sure you want to delete this app? This action cannot be undone.">
<button type="submit" class="btn-danger">Delete App</button>
</form>
</div>
</main>
{{end}}