feat: no-JS support for all core UI — registration, settings, monitor CRUD, logout

This commit is contained in:
2026-03-19 09:55:08 +04:00
parent 632f006988
commit 61560ae521
9 changed files with 194 additions and 117 deletions
+6 -2
View File
@@ -29,8 +29,12 @@
<p id="monitor-url" class="text-sm text-gray-500 mt-1"><%= m.url %></p>
</div>
<div class="flex items-center gap-3">
<button id="toggle-btn" class="text-sm px-4 py-2 rounded-lg border transition-colors <%= m.enabled ? 'border-gray-700 hover:border-gray-600 text-gray-300' : 'border-green-800 hover:border-green-700 text-green-400' %>"><%= m.enabled ? 'Pause' : 'Resume' %></button>
<button id="delete-btn" class="text-sm px-4 py-2 rounded-lg border border-red-900/50 text-red-400 hover:bg-red-900/20 transition-colors">Delete</button>
<form action="/dashboard/monitors/<%= m.id %>/toggle" method="POST" class="inline">
<button type="submit" id="toggle-btn" class="text-sm px-4 py-2 rounded-lg border transition-colors <%= m.enabled ? 'border-gray-700 hover:border-gray-600 text-gray-300' : 'border-green-800 hover:border-green-700 text-green-400' %>"><%= m.enabled ? 'Pause' : 'Resume' %></button>
</form>
<form action="/dashboard/monitors/<%= m.id %>/delete" method="POST" class="inline" onsubmit="return confirm('Delete this monitor? This cannot be undone.')">
<button type="submit" id="delete-btn" class="text-sm px-4 py-2 rounded-lg border border-red-900/50 text-red-400 hover:bg-red-900/20 transition-colors">Delete</button>
</form>
</div>
</div>
+10 -6
View File
@@ -33,10 +33,13 @@
<div class="mt-6 pt-5 border-t border-gray-800 text-center">
<p class="text-gray-500 text-sm mb-3">No account?</p>
<button id="register-btn"
class="w-full bg-gray-800 hover:bg-gray-700 border border-gray-700 text-gray-300 font-medium py-3 rounded-lg transition-colors">
Create Account
</button>
<form id="register-form" action="/account/register" method="POST">
<input type="hidden" name="_form" value="1">
<button type="submit" id="register-btn"
class="w-full bg-gray-800 hover:bg-gray-700 border border-gray-700 text-gray-300 font-medium py-3 rounded-lg transition-colors">
Create Account
</button>
</form>
</div>
</div>
@@ -102,8 +105,9 @@
} catch { showError('Connection error'); }
});
// Register
document.getElementById('register-btn').addEventListener('click', async () => {
// Register — JS-enhanced (overrides form POST for inline key display)
document.getElementById('register-form').addEventListener('submit', async (e) => {
e.preventDefault();
try {
const res = await fetch('/account/register', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}),
@@ -21,8 +21,8 @@
const bg = container.closest('form').querySelector('input')?.className.match(/bg-gray-\d+/)?.[0] || 'bg-gray-900';
const border = container.closest('form').querySelector('input')?.className.match(/border-gray-\d+/)?.[0] || 'border-gray-800';
row.innerHTML = `
<input type="text" placeholder="Header name" class="hk flex-1 ${bg} border ${border} rounded-lg px-3 py-2 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 text-sm">
<input type="text" placeholder="Value" class="hv flex-1 ${bg} border ${border} rounded-lg px-3 py-2 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 text-sm">
<input type="text" name="header_key" placeholder="Header name" class="hk flex-1 ${bg} border ${border} rounded-lg px-3 py-2 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 text-sm">
<input type="text" name="header_value" placeholder="Value" class="hv flex-1 ${bg} border ${border} rounded-lg px-3 py-2 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 text-sm">
<button type="button" onclick="this.parentElement.remove()" class="px-2 text-gray-600 hover:text-red-400 transition-colors text-sm">✕</button>
`;
container.appendChild(row);
+10 -10
View File
@@ -17,23 +17,23 @@
const bodyHidden = ['GET','HEAD','OPTIONS'].includes(curMethod);
%>
<form id="<%= formId %>" class="space-y-<%= isEdit ? '5' : '6' %>">
<form id="<%= formId %>" action="<%= isEdit ? '' : '/dashboard/monitors/new' %>" method="POST" class="space-y-<%= isEdit ? '5' : '6' %>">
<div>
<label class="block text-sm text-gray-400 mb-1.5">Name</label>
<input id="<%= prefix %>name" type="text" required value="<%= monitor.name || '' %>" placeholder="Production API"
<input id="<%= prefix %>name" name="name" type="text" required value="<%= monitor.name || '' %>" placeholder="Production API"
class="w-full <%= bg %> border <%= border %> rounded-lg px-4 py-2.5 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500">
</div>
<div>
<label class="block text-sm text-gray-400 mb-1.5">URL</label>
<div class="flex gap-2">
<select id="<%= prefix %>method"
<select id="<%= prefix %>method" name="method"
class="<%= bg %> border <%= border %> rounded-lg px-3 py-2.5 text-gray-100 focus:outline-none focus:border-blue-500 font-mono text-sm">
<% methods.forEach(function(method) { %>
<option <%= curMethod === method ? 'selected' : '' %>><%= method %></option>
<% }) %>
</select>
<input id="<%= prefix %>url" type="url" required value="<%= monitor.url || '' %>" placeholder="https://api.example.com/health"
<input id="<%= prefix %>url" name="url" type="url" required value="<%= monitor.url || '' %>" placeholder="https://api.example.com/health"
class="flex-1 <%= bg %> border <%= border %> rounded-lg px-4 py-2.5 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500">
</div>
</div>
@@ -48,8 +48,8 @@
<% if (monitor.request_headers && typeof monitor.request_headers === 'object') {
Object.entries(monitor.request_headers).forEach(function([k, v]) { %>
<div class="header-row flex gap-2">
<input type="text" value="<%= k %>" placeholder="Header name" class="hk flex-1 <%= bg %> border <%= border %> rounded-lg px-3 py-2 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 text-sm">
<input type="text" value="<%= v %>" placeholder="Value" class="hv flex-1 <%= bg %> border <%= border %> rounded-lg px-3 py-2 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 text-sm">
<input type="text" name="header_key" value="<%= k %>" placeholder="Header name" class="hk flex-1 <%= bg %> border <%= border %> rounded-lg px-3 py-2 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 text-sm">
<input type="text" name="header_value" value="<%= v %>" placeholder="Value" class="hv flex-1 <%= bg %> border <%= border %> rounded-lg px-3 py-2 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 text-sm">
<button type="button" onclick="this.parentElement.remove()" class="px-2 text-gray-600 hover:text-red-400 transition-colors text-sm">✕</button>
</div>
<% }) } %>
@@ -59,14 +59,14 @@
<!-- Request Body -->
<div id="<%= prefix %>body-section" class="<%= bodyHidden ? 'hidden' : '' %>">
<label class="block text-sm text-gray-400 mb-1.5">Request Body <span class="text-gray-600">(optional)</span></label>
<textarea id="<%= prefix %>request-body" rows="4" placeholder='{"key": "value"}'
<textarea id="<%= prefix %>request-body" name="request_body" rows="4" placeholder='{"key": "value"}'
class="w-full <%= bg %> border <%= border %> rounded-lg px-4 py-2.5 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 font-mono text-sm resize-y"><%= monitor.request_body || '' %></textarea>
</div>
<div class="flex gap-4">
<div class="flex-1">
<label class="block text-sm text-gray-400 mb-1.5"><%= isEdit ? 'Interval' : 'Ping Interval' %></label>
<select id="<%= prefix %>interval"
<select id="<%= prefix %>interval" name="interval_s"
class="w-full <%= bg %> border <%= border %> rounded-lg px-4 py-2.5 text-gray-100 focus:outline-none focus:border-blue-500">
<% intervals.forEach(function([val, label]) { %>
<option value="<%= val %>" <%= String(monitor.interval_s || '30') === val ? 'selected' : '' %>><%= label %></option>
@@ -75,7 +75,7 @@
</div>
<div class="flex-1">
<label class="block text-sm text-gray-400 mb-1.5">Timeout</label>
<select id="<%= prefix %>timeout"
<select id="<%= prefix %>timeout" name="timeout_ms"
class="w-full <%= bg %> border <%= border %> rounded-lg px-4 py-2.5 text-gray-100 focus:outline-none focus:border-blue-500">
<% timeouts.forEach(function([val, label]) { %>
<option value="<%= val %>" <%= String(monitor.timeout_ms || '10000') === val ? 'selected' : '' %>><%= label %></option>
@@ -93,7 +93,7 @@
<div class="flex flex-wrap gap-2">
<% regions.forEach(function([val, label]) { %>
<label class="flex items-center gap-2 <%= bg %> border <%= border %> hover:border-gray-600 rounded-lg px-3 py-2 cursor-pointer transition-colors">
<input type="checkbox" value="<%= val %>" class="<%= prefix %>region-check accent-blue-500" <%= selectedRegions.includes(val) ? 'checked' : '' %>> <span class="text-sm text-gray-300"><%~ label %></span>
<input type="checkbox" name="regions" value="<%= val %>" class="<%= prefix %>region-check accent-blue-500" <%= selectedRegions.includes(val) ? 'checked' : '' %>> <span class="text-sm text-gray-300"><%~ label %></span>
</label>
<% }) %>
</div>
+1 -1
View File
@@ -11,6 +11,6 @@
<% } else { %>
<a href="/dashboard/settings" class="hover:text-gray-300 transition-colors">Settings</a>
<% } %>
<button onclick="logout()" class="hover:text-gray-300 transition-colors">Logout</button>
<a href="/account/logout" class="hover:text-gray-300 transition-colors">Logout</a>
</div>
</nav>
+28 -72
View File
@@ -89,7 +89,10 @@
<div class="flex gap-2">
<code id="login-key-display" class="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-4 py-2.5 text-blue-400 text-sm font-mono select-all"><%= it.loginKey %></code>
<button onclick="copyLoginKey()" class="px-3 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg text-gray-400 hover:text-white text-xs transition-colors">Copy</button>
<button onclick="confirmReset()" class="px-3 bg-gray-800 hover:bg-gray-700 border border-red-900/50 hover:border-red-700/50 text-red-400 rounded-lg text-xs transition-colors">Rotate</button>
<form action="/account/reset-key" method="POST" class="inline" onsubmit="return confirm('Rotate your login key? Your current key stops working immediately.')">
<input type="hidden" name="_form" value="1">
<button type="submit" class="px-3 bg-gray-800 hover:bg-gray-700 border border-red-900/50 hover:border-red-700/50 text-red-400 rounded-lg text-xs transition-colors h-full py-2.5">Rotate</button>
</form>
</div>
</div>
<% } %>
@@ -112,13 +115,18 @@
<section class="bg-gray-900 rounded-xl border border-gray-800 p-6">
<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="<%= 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="<%= 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>
<form action="/account/email" method="POST">
<input type="hidden" name="_form" value="1">
<div class="flex gap-2">
<input id="email-input" name="email" 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 type="submit" 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>
<% if (hasEmail) { %>
<button type="submit" name="email" value="" class="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>
</form>
<p id="email-status" class="text-xs mt-2 hidden"></p>
</section>
<% } %>
@@ -133,19 +141,19 @@
<h2 class="text-sm font-semibold text-gray-300">Sub-Keys</h2>
<p class="text-xs text-gray-600 mt-0.5">Create separate keys for different apps, scripts, or teammates.</p>
</div>
<button onclick="showCreateKey()" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xs font-medium transition-colors">+ New Sub-Key</button>
<button onclick="document.getElementById('create-key-form').classList.toggle('hidden')" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xs font-medium transition-colors">+ New Sub-Key</button>
</div>
<!-- Create key form (hidden by default) -->
<div id="create-key-form" class="hidden mb-4 p-4 bg-gray-800/50 rounded-lg border border-gray-700">
<!-- Create key form -->
<form id="create-key-form" action="/account/keys" method="POST" class="hidden mb-4 p-4 bg-gray-800/50 rounded-lg border border-gray-700">
<input type="hidden" name="_form" value="1">
<label class="block text-xs text-gray-500 mb-1.5">Label</label>
<div class="flex gap-2">
<input id="key-label" type="text" placeholder="e.g. ci-pipeline, mobile-app"
<input name="label" type="text" placeholder="e.g. ci-pipeline, mobile-app" required
class="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 text-sm">
<button onclick="createKey()" class="px-4 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-sm font-medium transition-colors">Create</button>
<button onclick="hideCreateKey()" class="px-3 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg text-gray-500 text-sm transition-colors">Cancel</button>
<button type="submit" class="px-4 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-sm font-medium transition-colors">Create</button>
</div>
</div>
</form>
<!-- Sub-keys list -->
<div id="keys-list" class="space-y-2">
@@ -156,7 +164,10 @@
<div class="p-3 bg-gray-800/50 rounded-lg border border-gray-700/50">
<div class="flex items-center justify-between mb-2">
<p class="text-sm text-gray-200"><%= k.label %></p>
<button onclick="deleteKey('<%= k.id %>', this)" class="text-xs text-gray-600 hover:text-red-400 transition-colors px-2 py-1">Revoke</button>
<form action="/account/keys/<%= k.id %>/delete" method="POST" class="inline" onsubmit="return confirm('Revoke this key? Any apps using it will stop working.')">
<input type="hidden" name="_form" value="1">
<button type="submit" class="text-xs text-gray-600 hover:text-red-400 transition-colors px-2 py-1">Revoke</button>
</form>
</div>
<div class="flex gap-2">
<code class="flex-1 bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-blue-400 text-xs font-mono select-all"><%= k.key %></code>
@@ -174,31 +185,7 @@
<script>
async function saveEmail() {
const email = document.getElementById('email-input').value.trim();
if (!email) return;
try {
await api('/account/email', { method: 'POST', body: { email } });
location.reload();
} catch (e) {
showStatus('email-status', e.message, 'red');
}
}
async function removeEmail() {
if (!confirm('Remove recovery email?')) return;
await api('/account/email', { method: 'POST', body: { email: null } });
location.reload();
}
async function confirmReset() {
if (!confirm('Rotate your login key? Your current key stops working immediately.')) return;
const data = await api('/account/reset-key', { method: 'POST', body: {} });
document.getElementById('login-key-display').textContent = data.key;
}
// JS enhancements — clipboard copy (progressive, not required)
function copyLoginKey() {
const val = document.getElementById('login-key-display').textContent;
navigator.clipboard.writeText(val);
@@ -207,42 +194,11 @@
setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('text-green-400'); }, 1500);
}
function showCreateKey() {
document.getElementById('create-key-form').classList.remove('hidden');
document.getElementById('new-key-reveal').classList.add('hidden');
document.getElementById('key-label').focus();
}
function hideCreateKey() {
document.getElementById('create-key-form').classList.add('hidden');
}
async function createKey() {
const label = document.getElementById('key-label').value.trim();
if (!label) return;
await api('/account/keys', { method: 'POST', body: { label } });
location.reload();
}
function copyKey(btn, val) {
navigator.clipboard.writeText(val);
btn.textContent = 'Copied!'; btn.classList.add('text-green-400');
setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('text-green-400'); }, 1500);
}
async function deleteKey(id, btn) {
if (!confirm('Revoke this key? Any apps using it will stop working.')) return;
await api(`/account/keys/${id}`, { method: 'DELETE' });
btn.closest('.p-3').remove();
}
function showStatus(id, msg, color) {
const el = document.getElementById(id);
el.textContent = msg;
el.className = `text-xs mt-2 text-${color}-400`;
el.classList.remove('hidden');
setTimeout(() => el.classList.add('hidden'), 3000);
}
</script>
<%~ include('./partials/foot') %>
+35
View File
@@ -0,0 +1,35 @@
<%~ include('./partials/head', { title: 'Account Created' }) %>
<body class="bg-[#0a0a0a] text-gray-100 min-h-screen flex items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold tracking-tight">Ping<span class="text-blue-400">QL</span></h1>
</div>
<div class="bg-gray-900 rounded-xl p-6 border border-gray-800" style="box-shadow:0 0 40px rgba(59,130,246,0.08)">
<div class="flex items-center gap-3 mb-5">
<div class="w-8 h-8 rounded-full bg-green-500/20 flex items-center justify-center text-green-400 text-lg">✓</div>
<div>
<p class="font-semibold text-white">Account created</p>
<p class="text-xs text-gray-500">Save your key — it's how you access your account</p>
</div>
</div>
<label class="block text-xs text-gray-500 uppercase tracking-wider mb-2">Your Account Key</label>
<code class="block bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-blue-400 text-sm font-mono select-all break-all mb-5"><%= it.key %></code>
<div class="pt-5 border-t border-gray-800">
<form action="/account/email" method="POST" class="space-y-3">
<input type="hidden" name="_form" value="1">
<label class="block text-xs text-gray-500 uppercase tracking-wider mb-1">Email <span class="text-gray-600 normal-case">(optional — recovery only)</span></label>
<input name="email" type="email" placeholder="you@example.com"
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 text-sm">
<div class="flex gap-2">
<button type="submit" class="flex-1 bg-blue-600 hover:bg-blue-500 text-white font-medium py-2.5 rounded-lg transition-colors text-sm">Save & Continue</button>
<a href="/dashboard/home" class="px-4 bg-gray-800 hover:bg-gray-700 border border-gray-700 text-gray-400 rounded-lg transition-colors text-sm flex items-center">Skip</a>
</div>
</form>
</div>
</div>
</div>
</body>