refactor: full SSR dashboard, minimal SSE DOM patches, poll-based refresh

This commit is contained in:
M1
2026-03-16 21:14:45 +04:00
parent 878829111f
commit 2f7273604b
6 changed files with 299 additions and 376 deletions
+37 -60
View File
@@ -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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
loadSettings();
</script>
<%~ include('./partials/foot') %>