refactor: full SSR dashboard, minimal SSE DOM patches, poll-based refresh
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
<%~ include('./partials/head', { title: 'Settings' }) %>
|
||||
<%~ include('./partials/nav', { nav: 'settings' }) %>
|
||||
|
||||
<%
|
||||
const hasEmail = !!it.account.email_hash;
|
||||
const createdDate = new Date(it.account.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
%>
|
||||
|
||||
<main class="max-w-3xl mx-auto px-8 py-10 space-y-8">
|
||||
|
||||
<h1 class="text-xl font-semibold text-white">Settings</h1>
|
||||
@@ -12,13 +17,13 @@
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Primary Key</label>
|
||||
<div class="flex gap-2">
|
||||
<code id="primary-key" class="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-4 py-2.5 text-blue-400 text-sm tracking-widest"></code>
|
||||
<code id="primary-key" class="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-4 py-2.5 text-blue-400 text-sm tracking-widest"><%= it.account.id %></code>
|
||||
<button onclick="copyKey()" class="px-3 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg text-gray-400 hover:text-white transition-colors text-xs">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Member since</label>
|
||||
<p id="created-at" class="text-sm text-gray-400"></p>
|
||||
<p id="created-at" class="text-sm text-gray-400"><%= createdDate %></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -28,11 +33,11 @@
|
||||
<h2 class="text-sm font-semibold text-gray-300 mb-1">Recovery Email</h2>
|
||||
<p class="text-xs text-gray-600 mb-4">Used for account recovery only. Stored as a one-way hash — we can't read it.</p>
|
||||
<div class="flex gap-2">
|
||||
<input id="email-input" type="email" placeholder="you@example.com"
|
||||
<input id="email-input" type="email" placeholder="<%= hasEmail ? '●●●●●●●● (set)' : 'you@example.com' %>"
|
||||
class="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-4 py-2.5 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 text-sm">
|
||||
<button onclick="saveEmail()" id="email-btn"
|
||||
class="px-4 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-sm font-medium transition-colors">Save</button>
|
||||
<button onclick="removeEmail()" id="remove-email-btn" class="hidden px-3 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg text-gray-500 hover:text-red-400 text-xs transition-colors">Remove</button>
|
||||
<button onclick="removeEmail()" id="remove-email-btn" class="<%= hasEmail ? '' : 'hidden' %> px-3 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg text-gray-500 hover:text-red-400 text-xs transition-colors">Remove</button>
|
||||
</div>
|
||||
<p id="email-status" class="text-xs mt-2 hidden"></p>
|
||||
</section>
|
||||
@@ -76,46 +81,38 @@
|
||||
|
||||
<!-- Keys list -->
|
||||
<div id="keys-list" class="space-y-2">
|
||||
<p class="text-xs text-gray-600 italic">No API keys yet.</p>
|
||||
<% if (it.apiKeys.length === 0) { %>
|
||||
<p class="text-xs text-gray-600 italic">No API keys yet.</p>
|
||||
<% } else { %>
|
||||
<% it.apiKeys.forEach(function(k) { %>
|
||||
<div class="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg border border-gray-700/50">
|
||||
<div>
|
||||
<p class="text-sm text-gray-200"><%= k.label %></p>
|
||||
<p class="text-xs text-gray-600 mt-0.5 font-mono"><%= k.id %> · created <%= new Date(k.created_at).toLocaleDateString() %> <%~ k.last_used_at ? '· last used ' + it.timeAgoSSR(k.last_used_at) : '· never used' %></p>
|
||||
</div>
|
||||
<button onclick="deleteKey('<%= k.id %>')" class="text-xs text-gray-600 hover:text-red-400 transition-colors px-2 py-1">Revoke</button>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
async function loadSettings() {
|
||||
const data = await api('/account/settings');
|
||||
|
||||
document.getElementById('primary-key').textContent = data.account_id;
|
||||
document.getElementById('created-at').textContent = new Date(data.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
|
||||
if (data.has_email) {
|
||||
document.getElementById('remove-email-btn').classList.remove('hidden');
|
||||
document.getElementById('email-input').placeholder = '●●●●●●●● (set)';
|
||||
// On load: check sessionStorage for a newly created key to reveal
|
||||
(function() {
|
||||
const newKey = sessionStorage.getItem('_newApiKey');
|
||||
if (newKey) {
|
||||
sessionStorage.removeItem('_newApiKey');
|
||||
document.getElementById('new-key-value').textContent = newKey;
|
||||
document.getElementById('new-key-reveal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
renderKeys(data.api_keys);
|
||||
}
|
||||
|
||||
function renderKeys(keys) {
|
||||
const el = document.getElementById('keys-list');
|
||||
if (!keys.length) {
|
||||
el.innerHTML = '<p class="text-xs text-gray-600 italic">No API keys yet.</p>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = keys.map(k => `
|
||||
<div class="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg border border-gray-700/50">
|
||||
<div>
|
||||
<p class="text-sm text-gray-200">${escapeHtml(k.label)}</p>
|
||||
<p class="text-xs text-gray-600 mt-0.5 font-mono">${k.id} · created ${new Date(k.created_at).toLocaleDateString()} ${k.last_used_at ? `· last used ${timeAgo(k.last_used_at)}` : '· never used'}</p>
|
||||
</div>
|
||||
<button onclick="deleteKey('${k.id}')" class="text-xs text-gray-600 hover:text-red-400 transition-colors px-2 py-1">Revoke</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
})();
|
||||
|
||||
function copyKey() {
|
||||
const key = document.getElementById('primary-key').textContent;
|
||||
navigator.clipboard.writeText(key);
|
||||
const btn = event.target;
|
||||
btn.textContent = 'Copied!';
|
||||
@@ -125,35 +122,24 @@
|
||||
async function saveEmail() {
|
||||
const email = document.getElementById('email-input').value.trim();
|
||||
if (!email) return;
|
||||
const btn = document.getElementById('email-btn');
|
||||
btn.disabled = true; btn.textContent = 'Saving...';
|
||||
try {
|
||||
await api('/account/email', { method: 'POST', body: { email } });
|
||||
showStatus('email-status', 'Saved.', 'green');
|
||||
document.getElementById('remove-email-btn').classList.remove('hidden');
|
||||
document.getElementById('email-input').value = '';
|
||||
document.getElementById('email-input').placeholder = '●●●●●●●● (set)';
|
||||
location.reload();
|
||||
} catch (e) {
|
||||
showStatus('email-status', e.message, 'red');
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = 'Save';
|
||||
}
|
||||
}
|
||||
|
||||
async function removeEmail() {
|
||||
if (!confirm('Remove recovery email?')) return;
|
||||
await api('/account/email', { method: 'POST', body: { email: null } });
|
||||
document.getElementById('remove-email-btn').classList.add('hidden');
|
||||
document.getElementById('email-input').placeholder = 'you@example.com';
|
||||
showStatus('email-status', 'Email removed.', 'gray');
|
||||
location.reload();
|
||||
}
|
||||
|
||||
async function confirmReset() {
|
||||
if (!confirm('Rotate your primary key?\n\nYour current key will stop working immediately. Make sure to copy the new one.')) return;
|
||||
const data = await api('/account/reset-key', { method: 'POST', body: {} });
|
||||
// Re-auth with new key (updates cookie)
|
||||
await fetch('/account/login', { method: 'POST', credentials: 'same-origin', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ key: data.key }) });
|
||||
document.getElementById('primary-key').textContent = data.key;
|
||||
location.reload();
|
||||
}
|
||||
|
||||
@@ -171,11 +157,8 @@
|
||||
const label = document.getElementById('key-label').value.trim();
|
||||
if (!label) return;
|
||||
const data = await api('/account/keys', { method: 'POST', body: { label } });
|
||||
document.getElementById('new-key-value').textContent = data.key;
|
||||
document.getElementById('new-key-reveal').classList.remove('hidden');
|
||||
hideCreateKey();
|
||||
document.getElementById('key-label').value = '';
|
||||
loadSettings();
|
||||
sessionStorage.setItem('_newApiKey', data.key);
|
||||
location.reload();
|
||||
}
|
||||
|
||||
function copyNewKey() {
|
||||
@@ -190,7 +173,7 @@
|
||||
async function deleteKey(id) {
|
||||
if (!confirm('Revoke this key? Any apps using it will stop working.')) return;
|
||||
await api(`/account/keys/${id}`, { method: 'DELETE' });
|
||||
loadSettings();
|
||||
location.reload();
|
||||
}
|
||||
|
||||
function showStatus(id, msg, color) {
|
||||
@@ -200,12 +183,6 @@
|
||||
el.classList.remove('hidden');
|
||||
setTimeout(() => el.classList.add('hidden'), 3000);
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
loadSettings();
|
||||
</script>
|
||||
|
||||
<%~ include('./partials/foot') %>
|
||||
|
||||
Reference in New Issue
Block a user