mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-04-20 16:40:29 +00:00
314 lines
21 KiB
HTML
314 lines
21 KiB
HTML
{{ define "settings" }}
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
{{ template "head" . }}
|
|
{{ template "meta" .Meta }}
|
|
</head>
|
|
<body>
|
|
{{ template "nav" . }}
|
|
|
|
<main id="main-content" class="container mx-auto px-4 py-8">
|
|
<h1 class="text-3xl font-display font-bold tracking-tight mb-6">Settings</h1>
|
|
|
|
<!-- Mobile identity info (below lg) -->
|
|
<div class="lg:hidden mb-4 space-y-1 text-xs text-base-content/70">
|
|
<div class="break-all"><code>{{ .Profile.DID }}</code></div>
|
|
<div><a href="{{ .Profile.PDSEndpoint }}/account" target="_blank" class="link link-primary inline-flex items-center gap-1">{{ .Profile.PDSEndpoint }} {{ icon "external-link" "size-3" }}</a></div>
|
|
</div>
|
|
|
|
<!-- Mobile tab bar (below lg) -->
|
|
<div class="flex gap-2 overflow-x-auto pb-2 lg:hidden mb-6" role="tablist" aria-label="Settings sections" aria-orientation="horizontal">
|
|
<button class="btn btn-sm btn-ghost settings-tab-mobile" data-tab="user" role="tab" id="tab-mobile-user" aria-controls="tab-user" aria-selected="false" tabindex="-1">
|
|
{{ icon "user" "size-4" }} User
|
|
</button>
|
|
<button class="btn btn-sm btn-ghost settings-tab-mobile" data-tab="billing" role="tab" id="tab-mobile-billing" aria-controls="tab-billing" aria-selected="false" tabindex="-1">
|
|
{{ icon "credit-card" "size-4" }} Billing
|
|
</button>
|
|
<button class="btn btn-sm btn-ghost settings-tab-mobile" data-tab="storage" role="tab" id="tab-mobile-storage" aria-controls="tab-storage" aria-selected="false" tabindex="-1">
|
|
{{ icon "hard-drive" "size-4" }} Storage
|
|
</button>
|
|
<button class="btn btn-sm btn-ghost settings-tab-mobile" data-tab="devices" role="tab" id="tab-mobile-devices" aria-controls="tab-devices" aria-selected="false" tabindex="-1">
|
|
{{ icon "terminal" "size-4" }} Devices
|
|
</button>
|
|
<button class="btn btn-sm btn-ghost settings-tab-mobile" data-tab="webhooks" role="tab" id="tab-mobile-webhooks" aria-controls="tab-webhooks" aria-selected="false" tabindex="-1">
|
|
{{ icon "webhook" "size-4" }} Webhooks
|
|
</button>
|
|
<button class="btn btn-sm btn-ghost settings-tab-mobile" data-tab="advanced" role="tab" id="tab-mobile-advanced" aria-controls="tab-advanced" aria-selected="false" tabindex="-1">
|
|
{{ icon "shield-check" "size-4" }} Advanced
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex gap-8">
|
|
<!-- Sidebar (lg and above) -->
|
|
<aside class="hidden lg:block w-56 shrink-0">
|
|
<ul class="menu bg-base-200 rounded-box w-full" role="tablist" aria-label="Settings sections" aria-orientation="vertical">
|
|
<li data-tab="user" role="none"><a href="#user" role="tab" id="tab-desktop-user" aria-controls="tab-user" aria-selected="false" tabindex="-1">{{ icon "user" "size-4" }} User</a></li>
|
|
<li data-tab="billing" role="none"><a href="#billing" role="tab" id="tab-desktop-billing" aria-controls="tab-billing" aria-selected="false" tabindex="-1">{{ icon "credit-card" "size-4" }} Billing</a></li>
|
|
<li data-tab="storage" role="none"><a href="#storage" role="tab" id="tab-desktop-storage" aria-controls="tab-storage" aria-selected="false" tabindex="-1">{{ icon "hard-drive" "size-4" }} Storage</a></li>
|
|
<li data-tab="devices" role="none"><a href="#devices" role="tab" id="tab-desktop-devices" aria-controls="tab-devices" aria-selected="false" tabindex="-1">{{ icon "terminal" "size-4" }} Devices</a></li>
|
|
<li data-tab="webhooks" role="none"><a href="#webhooks" role="tab" id="tab-desktop-webhooks" aria-controls="tab-webhooks" aria-selected="false" tabindex="-1">{{ icon "webhook" "size-4" }} Webhooks</a></li>
|
|
<li data-tab="advanced" role="none"><a href="#advanced" role="tab" id="tab-desktop-advanced" aria-controls="tab-advanced" aria-selected="false" tabindex="-1">{{ icon "shield-check" "size-4" }} Advanced</a></li>
|
|
</ul>
|
|
<div class="mt-4 px-2 space-y-1 text-xs text-base-content/70">
|
|
<div class="break-all"><code>{{ .Profile.DID }}</code></div>
|
|
<div><a href="{{ .Profile.PDSEndpoint }}/account" target="_blank" class="link link-primary inline-flex items-center gap-1">{{ .Profile.PDSEndpoint }} {{ icon "external-link" "size-3" }}</a></div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Tab content -->
|
|
<div class="flex-1 min-w-0">
|
|
|
|
<!-- USER TAB -->
|
|
<div id="tab-user" class="settings-panel hidden space-y-6" role="tabpanel" aria-labelledby="tab-desktop-user" tabindex="0">
|
|
<section class="card bg-base-200 shadow-sm p-6 space-y-6">
|
|
<div>
|
|
<h2 class="text-xl font-semibold">Preferences</h2>
|
|
<p class="text-base-content/70 mt-1">Customize your experience across the site.</p>
|
|
</div>
|
|
|
|
<!-- Preferred Client Selector -->
|
|
<div class="flex items-center gap-4">
|
|
<div>
|
|
<label for="oci-client-select" class="text-sm font-medium">Preferred client</label>
|
|
<p id="oci-client-hint" class="text-xs text-base-content/70">Sets the pull command shown on repository pages. Choose <em>Image reference only</em> to copy without a command prefix.</p>
|
|
</div>
|
|
{{ $oci := .Profile.OciClient }}
|
|
<select id="oci-client-select" aria-describedby="oci-client-hint" class="select select-sm select-bordered min-w-40"
|
|
name="oci_client"
|
|
hx-post="/api/profile/oci-client"
|
|
hx-trigger="change"
|
|
hx-swap="none">
|
|
<option value="docker"{{ if or (eq $oci "") (eq $oci "docker") }} selected{{ end }}>Docker</option>
|
|
<option value="podman"{{ if eq $oci "podman" }} selected{{ end }}>Podman</option>
|
|
<option value="buildah"{{ if eq $oci "buildah" }} selected{{ end }}>Buildah</option>
|
|
<option value="nerdctl"{{ if eq $oci "nerdctl" }} selected{{ end }}>nerdctl</option>
|
|
<option value="crane"{{ if eq $oci "crane" }} selected{{ end }}>crane</option>
|
|
<option value="none"{{ if eq $oci "none" }} selected{{ end }}>Image reference only</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- AI Image Advisor Toggle -->
|
|
{{ if .AIAdvisorEnabled }}
|
|
<div class="divider my-2"></div>
|
|
<div class="flex items-start gap-3">
|
|
{{ if .Profile.HasAIAdvisorAccess }}
|
|
<label class="flex items-start gap-3 cursor-pointer">
|
|
<input type="checkbox" class="toggle toggle-primary mt-0.5"
|
|
hx-post="/api/profile/ai-advisor"
|
|
hx-trigger="change"
|
|
hx-swap="none"
|
|
{{ if .Profile.AIAdvisorEnabled }}checked{{ end }}>
|
|
<div>
|
|
<span class="font-medium">AI Image Advisor</span>
|
|
<p class="text-xs text-base-content/60">Analyze your container images for optimization suggestions using AI.</p>
|
|
</div>
|
|
</label>
|
|
{{ else }}
|
|
<div>
|
|
<span class="font-medium text-base-content/50">AI Image Advisor</span>
|
|
<p class="text-xs text-base-content/70">Analyze your container images for optimization suggestions using AI.</p>
|
|
<p class="text-xs text-primary mt-1">
|
|
<a href="/settings#billing">Upgrade your plan</a> to enable this feature.
|
|
</p>
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
{{ end }}
|
|
</section>
|
|
</div>
|
|
|
|
<!-- STORAGE TAB -->
|
|
<div id="tab-storage" class="settings-panel hidden space-y-4" role="tabpanel" aria-labelledby="tab-desktop-storage" tabindex="0">
|
|
<!-- Holds -->
|
|
{{ if .AllHolds }}
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
<div class="space-y-4">
|
|
{{ template "hold_selector" . }}
|
|
{{ if .ActiveHold }}
|
|
{{ template "hold_card" .ActiveHold }}
|
|
{{ else }}
|
|
<div class="card bg-base-200 shadow-sm p-6 text-center text-base-content/60">
|
|
No active hold selected. Choose one above.
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
<div>
|
|
{{ if .OtherHolds }}
|
|
{{ template "other_holds_table" .OtherHolds }}
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
{{ else }}
|
|
<div class="card bg-base-200 shadow-sm p-6 text-center text-base-content/60">
|
|
No holds configured. Push an image to get started.
|
|
</div>
|
|
{{ end }}
|
|
|
|
<!-- Storage Preferences -->
|
|
<section class="card bg-base-200 shadow-sm p-6 space-y-4">
|
|
<h2 class="text-xl font-semibold">Storage Preferences</h2>
|
|
<label class="flex items-start gap-3 cursor-pointer">
|
|
<input type="checkbox" class="toggle toggle-primary mt-0.5"
|
|
hx-post="/api/profile/auto-remove-untagged"
|
|
hx-trigger="change"
|
|
hx-swap="none"
|
|
{{ if .Profile.AutoRemoveUntagged }}checked{{ end }}>
|
|
<div>
|
|
<span class="font-medium">Automatically remove untagged manifests</span>
|
|
<p class="text-sm text-base-content/60 mt-1">
|
|
When a tag is overwritten, the old manifest and its layers are cleaned up.
|
|
Multi-arch child manifests are preserved.
|
|
</p>
|
|
</div>
|
|
</label>
|
|
</section>
|
|
</div>
|
|
|
|
<!-- BILLING TAB -->
|
|
<div id="tab-billing" class="settings-panel hidden space-y-4" role="tabpanel" aria-labelledby="tab-desktop-billing" tabindex="0">
|
|
{{ template "subscription_plans" .Subscription }}
|
|
</div>
|
|
|
|
<!-- DEVICES TAB -->
|
|
<div id="tab-devices" class="settings-panel hidden space-y-6" role="tabpanel" aria-labelledby="tab-desktop-devices" tabindex="0">
|
|
<section class="card bg-base-200 shadow-sm p-6 space-y-6">
|
|
<div>
|
|
<h2 class="text-xl font-semibold">Authorized Devices</h2>
|
|
<p class="text-base-content/70 mt-1">Devices authorized via <code class="cmd">docker-credential-atcr</code> credential helper.</p>
|
|
</div>
|
|
|
|
<!-- Setup Instructions -->
|
|
<div class="bg-base-200 rounded-lg p-4 space-y-4">
|
|
<h3 class="font-semibold">First Time Setup</h3>
|
|
<ol class="list-decimal list-inside space-y-4 text-sm">
|
|
<li>Install credential helper:
|
|
<pre class="mt-2 p-3 bg-base-300 rounded-lg overflow-x-auto"><code>curl -fsSL {{ .SiteURL }}/static/install.sh | bash</code></pre>
|
|
</li>
|
|
<li>Configure Docker to use the helper. Add to <code class="cmd">~/.docker/config.json</code>:
|
|
<pre class="mt-2 p-3 bg-base-300 rounded-lg overflow-x-auto"><code>{
|
|
"credHelpers": {
|
|
"{{ .RegistryURL }}": "atcr"
|
|
}
|
|
}</code></pre>
|
|
</li>
|
|
<li>Run any Docker command:
|
|
<div class="mt-2">{{ template "docker-command" (print (pullPrefix .OciClient) .RegistryURL "/" .Profile.Handle "/myimage") }}</div>
|
|
</li>
|
|
<li>Browser will open for authorization - click Approve</li>
|
|
<li>Done! Device is automatically authorized</li>
|
|
</ol>
|
|
|
|
<div class="pt-3 border-t border-base-300 text-sm">
|
|
<strong>Fallback:</strong> Use <a href="https://bsky.app/settings/app-passwords" target="_blank" class="link link-primary">app password</a> with <code class="cmd">docker login {{ .RegistryURL }}</code> for quick start (no device tracking)
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Devices List -->
|
|
<div class="space-y-3">
|
|
<h3 class="font-semibold">Your Authorized Devices</h3>
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-zebra">
|
|
<thead>
|
|
<tr>
|
|
<th>Device Name</th>
|
|
<th>IP Address</th>
|
|
<th>Created</th>
|
|
<th>Last Used</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="devices-table"
|
|
hx-get="/api/devices"
|
|
hx-trigger="tab:devices from:body once, every 30s[isTabActive('devices')], devicesChanged from:body"
|
|
hx-swap="innerHTML">
|
|
<tr><td colspan="5" class="text-center">{{ icon "loader-2" "size-4 animate-spin inline-block" }} Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<!-- WEBHOOKS TAB -->
|
|
<div id="tab-webhooks" class="settings-panel hidden space-y-6" role="tabpanel" aria-labelledby="tab-desktop-webhooks" tabindex="0">
|
|
<section class="card bg-base-200 shadow-sm p-6 space-y-4">
|
|
<div>
|
|
<h2 class="text-xl font-semibold">Webhooks</h2>
|
|
<p class="text-base-content/70 mt-1">Get notified when images are pushed or vulnerability scans complete.</p>
|
|
</div>
|
|
<div id="webhooks-content">
|
|
{{ template "webhooks_list" .WebhooksData }}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<!-- ADVANCED TAB -->
|
|
<div id="tab-advanced" class="settings-panel hidden space-y-6" role="tabpanel" aria-labelledby="tab-desktop-advanced" tabindex="0">
|
|
<!-- Data Privacy Section -->
|
|
<section class="card bg-base-200 shadow-sm p-6 space-y-4">
|
|
<h2 class="text-xl font-semibold">Data Privacy</h2>
|
|
<p class="text-base-content/70">Download a copy of all data we store about you.</p>
|
|
|
|
<div>
|
|
<a href="/api/export-data" class="btn btn-secondary gap-2" download>
|
|
{{ icon "download" "size-4" }}
|
|
Export All My Data
|
|
</a>
|
|
</div>
|
|
|
|
<p class="text-sm text-base-content/60">
|
|
This includes your authorized devices, sessions, and hold memberships.
|
|
Data stored on your PDS is already under your control.
|
|
See our <a href="/privacy" class="link link-primary">Privacy Policy</a> for details.
|
|
</p>
|
|
</section>
|
|
|
|
<!-- Danger Zone Section -->
|
|
<section class="border-2 border-error rounded-lg p-6 space-y-4">
|
|
<h2 class="text-xl font-semibold text-error flex items-center gap-2">
|
|
{{ icon "alert-triangle" "size-5" }}
|
|
Danger Zone
|
|
</h2>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<h3 class="font-semibold">Delete {{ .ClientShortName }} Data</h3>
|
|
<p class="text-base-content/70 mt-1">Remove your data from {{ .ClientShortName }}. This action cannot be undone.</p>
|
|
</div>
|
|
|
|
<div class="alert bg-base-200">
|
|
{{ icon "info" "size-5 shrink-0" }}
|
|
<span><strong>This does not delete your ATProto (Bluesky, Blacksky, Tangled) account.</strong><br>Only {{ .ClientShortName }}-specific data (authorized devices, hold memberships, settings) will be removed.</span>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<label class="flex items-start gap-3 cursor-pointer">
|
|
<input type="checkbox" id="delete-pds-records" class="checkbox checkbox-sm mt-0.5">
|
|
<span class="text-sm">Also delete all <code class="cmd">io.atcr.*</code> records from my ATProto PDS</span>
|
|
</label>
|
|
<p class="text-xs text-base-content/60 ml-7">
|
|
This removes {{ .ClientShortName }} records (manifests, tags, stars, profile) stored in your PDS.
|
|
Other records in your account are not impacted.
|
|
</p>
|
|
</div>
|
|
|
|
<button type="button" id="delete-account-btn" class="btn btn-error btn-lg gap-2"
|
|
data-client-short-name="{{ .ClientShortName }}"
|
|
data-profile-handle="{{ .Profile.Handle }}">
|
|
{{ icon "trash-2" "size-5" }}
|
|
Delete My {{ .ClientShortName }} Data
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
{{ template "footer" . }}
|
|
</body>
|
|
</html>
|
|
{{ end }}
|