mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-30 12:46:57 +00:00
282 lines
9.1 KiB
HTML
282 lines
9.1 KiB
HTML
{{ define "settings" }}
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<title>Settings - ATCR</title>
|
|
{{ template "head" . }}
|
|
</head>
|
|
<body>
|
|
{{ template "nav" . }}
|
|
|
|
<main class="container">
|
|
<div class="settings-page">
|
|
<h1>Settings</h1>
|
|
|
|
<!-- Identity Section -->
|
|
<section class="settings-section">
|
|
<h2>Identity</h2>
|
|
<div class="form-group">
|
|
<label>Handle:</label>
|
|
<span>{{ .Profile.Handle }}</span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>DID:</label>
|
|
<code>{{ .Profile.DID }}</code>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>PDS:</label>
|
|
<span>{{ .Profile.PDSEndpoint }}</span>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Default Hold Section -->
|
|
<section class="settings-section">
|
|
<h2>Default Hold</h2>
|
|
<p>Current: <strong id="current-hold">{{ if .Profile.DefaultHold }}{{ .Profile.DefaultHold }}{{ else }}Not set{{ end }}</strong></p>
|
|
|
|
<form hx-post="/api/profile/default-hold"
|
|
hx-target="#hold-status"
|
|
hx-swap="innerHTML"
|
|
id="hold-form">
|
|
|
|
<div class="form-group">
|
|
<label for="hold-endpoint">Hold Endpoint:</label>
|
|
<input type="text"
|
|
id="hold-endpoint"
|
|
name="hold_endpoint"
|
|
value="{{ .Profile.DefaultHold }}"
|
|
placeholder="https://hold.example.com" />
|
|
<small>Leave empty to use AppView default storage</small>
|
|
</div>
|
|
|
|
<button type="submit" class="btn-primary">Save</button>
|
|
</form>
|
|
|
|
<div id="hold-status"></div>
|
|
</section>
|
|
|
|
<!-- Authorized Devices Section -->
|
|
<section class="settings-section devices-section">
|
|
<h2>Authorized Devices</h2>
|
|
<p>Devices authorized via <code>docker-credential-atcr</code> credential helper.</p>
|
|
|
|
<!-- Setup Instructions -->
|
|
<div class="setup-instructions">
|
|
<h3>First Time Setup</h3>
|
|
<ol>
|
|
<li>Install credential helper:
|
|
<pre><code>curl -fsSL atcr.io/static/install.sh | bash</code></pre>
|
|
</li>
|
|
<li>Configure Docker to use the helper. Add to <code>~/.docker/config.json</code>:
|
|
<pre><code>{
|
|
"credHelpers": {
|
|
"{{ .RegistryURL }}": "atcr"
|
|
}
|
|
}</code></pre>
|
|
</li>
|
|
<li>Run any Docker command:
|
|
{{ template "docker-command" (print "docker pull " .RegistryURL "/" .Profile.Handle "/myimage") }}
|
|
</li>
|
|
<li>Browser will open for authorization - click Approve</li>
|
|
<li>Done! Device is automatically authorized</li>
|
|
</ol>
|
|
|
|
<div class="fallback-note">
|
|
<strong>Fallback:</strong> Use <a href="https://bsky.app/settings/app-passwords" target="_blank">app password</a> with <code>docker login {{ .RegistryURL }}</code> for quick start (no device tracking)
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Devices List -->
|
|
<div class="devices-list">
|
|
<h3>Your Authorized Devices</h3>
|
|
<table>
|
|
<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">
|
|
<tr><td colspan="5">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
// Default Hold Update - Dynamic display update
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const holdForm = document.getElementById('hold-form');
|
|
|
|
holdForm.addEventListener('htmx:afterSwap', function(event) {
|
|
// Check if the response contains success indicator
|
|
if (event.detail.xhr.status === 200) {
|
|
const holdInput = document.getElementById('hold-endpoint');
|
|
const currentHoldDisplay = document.getElementById('current-hold');
|
|
const newValue = holdInput.value.trim();
|
|
|
|
// Update the current hold display
|
|
currentHoldDisplay.textContent = newValue || 'Not set';
|
|
}
|
|
});
|
|
});
|
|
|
|
// Device Management JavaScript
|
|
(function() {
|
|
// Load devices
|
|
async function loadDevices() {
|
|
try {
|
|
const resp = await fetch('/api/devices');
|
|
if (!resp.ok) {
|
|
throw new Error('Failed to load devices');
|
|
}
|
|
|
|
const devices = await resp.json();
|
|
const tbody = document.getElementById('devices-table');
|
|
|
|
if (devices.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="5">No authorized devices yet. Follow the setup instructions above!</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = devices.map(device => {
|
|
const createdDate = new Date(device.created_at).toLocaleDateString();
|
|
const lastUsed = device.last_used && device.last_used !== '0001-01-01T00:00:00Z'
|
|
? new Date(device.last_used).toLocaleDateString()
|
|
: 'Never';
|
|
|
|
return `
|
|
<tr>
|
|
<td>${escapeHtml(device.name)}</td>
|
|
<td>${escapeHtml(device.ip_address || 'Unknown')}</td>
|
|
<td>${createdDate}</td>
|
|
<td>${lastUsed}</td>
|
|
<td><button class="btn-danger" onclick="revokeDevice('${device.id}')">Revoke</button></td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
} catch (err) {
|
|
console.error('Error loading devices:', err);
|
|
document.getElementById('devices-table').innerHTML =
|
|
'<tr><td colspan="5">Error loading devices</td></tr>';
|
|
}
|
|
}
|
|
|
|
// Revoke device
|
|
window.revokeDevice = async function(id) {
|
|
if (!confirm('Are you sure you want to revoke this device? This cannot be undone.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const resp = await fetch(`/api/devices/${id}`, { method: 'DELETE' });
|
|
if (!resp.ok) {
|
|
throw new Error('Failed to revoke device');
|
|
}
|
|
loadDevices();
|
|
} catch (err) {
|
|
alert('Error revoking device: ' + err.message);
|
|
}
|
|
};
|
|
|
|
// Escape HTML helper
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Load devices on page load
|
|
loadDevices();
|
|
|
|
// Refresh devices every 30 seconds (to show new authorizations)
|
|
setInterval(loadDevices, 30000);
|
|
})();
|
|
</script>
|
|
|
|
<style>
|
|
/* Devices Section Styles */
|
|
.devices-section .setup-instructions {
|
|
margin: 1rem 0;
|
|
padding: 1.5rem;
|
|
background: var(--code-bg);
|
|
border-radius: 4px;
|
|
}
|
|
.devices-section .setup-instructions h3 {
|
|
margin-top: 0;
|
|
}
|
|
.devices-section .setup-instructions ol {
|
|
margin-left: 1.5rem;
|
|
}
|
|
.devices-section .setup-instructions li {
|
|
margin-bottom: 1rem;
|
|
}
|
|
.devices-section .setup-instructions pre {
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
border: 1px solid var(--border);
|
|
padding: 0.75rem;
|
|
border-radius: 4px;
|
|
overflow-x: auto;
|
|
margin: 0.5rem 0;
|
|
}
|
|
.devices-section .setup-instructions code {
|
|
font-family: monospace;
|
|
}
|
|
.devices-section .fallback-note {
|
|
margin-top: 1rem;
|
|
padding: 1rem;
|
|
background: var(--warning-bg);
|
|
border: 1px solid var(--warning);
|
|
border-radius: 4px;
|
|
}
|
|
.devices-section .fallback-note a {
|
|
color: var(--warning);
|
|
text-decoration: underline;
|
|
font-weight: 500;
|
|
}
|
|
.devices-section .fallback-note a:hover {
|
|
color: var(--primary);
|
|
}
|
|
.devices-section .fallback-note a:visited {
|
|
color: var(--warning);
|
|
}
|
|
.devices-section table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-top: 1rem;
|
|
}
|
|
.devices-section th,
|
|
.devices-section td {
|
|
padding: 0.75rem;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.devices-section th {
|
|
background: var(--code-bg);
|
|
font-weight: bold;
|
|
}
|
|
.devices-section .btn-danger {
|
|
background: #dc3545;
|
|
color: white;
|
|
border: none;
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
.devices-section .btn-danger:hover {
|
|
background: #c82333;
|
|
}
|
|
.devices-list {
|
|
margin-top: 2rem;
|
|
}
|
|
</style>
|
|
</body>
|
|
</html>
|
|
{{ end }}
|