pingql/apps/web/src/views/settings.ejs

209 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<%~ 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' });
const plan = it.account.plan || 'free';
const planLabel = { free: 'Free', pro: 'Pro', lifetime: 'Lifetime' }[plan] || plan;
const limits = { free: { monitors: 10, interval: '30s' }, pro: { monitors: 500, interval: '2s' }, lifetime: { monitors: 500, interval: '2s' } }[plan] || { monitors: 10, interval: '30s' };
%>
<main class="max-w-3xl mx-auto px-8 py-10 space-y-8">
<h1 class="text-xl font-semibold text-white">Settings</h1>
<!-- Plan -->
<section class="card-static p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-semibold text-gray-300">Plan</h2>
<% if (plan === 'free') { %>
<span class="text-xs font-medium px-2.5 py-1 rounded-full bg-gray-800/50 border border-border-subtle text-gray-400">Free</span>
<% } else if (plan === 'pro') { %>
<span class="text-xs font-medium px-2.5 py-1 rounded-full bg-gradient-to-r from-blue-600/20 to-blue-500/10 border border-blue-500/30 text-blue-400">Pro</span>
<% } else if (plan === 'lifetime') { %>
<span class="text-xs font-medium px-2.5 py-1 rounded-full bg-gradient-to-r from-yellow-600/20 to-yellow-500/10 border border-yellow-500/30 text-yellow-400">Lifetime</span>
<% } %>
</div>
<div class="grid grid-cols-3 gap-4 text-center">
<div>
<div class="text-lg font-semibold text-gray-200"><%= it.monitorCount %><span class="text-gray-600 text-sm font-normal">/<%= limits.monitors %></span></div>
<div class="text-xs text-gray-500">Monitors</div>
</div>
<div>
<div class="text-lg font-semibold text-gray-200"><%= limits.interval %></div>
<div class="text-xs text-gray-500">Min Interval</div>
</div>
<div>
<div class="text-lg font-semibold text-gray-200">2</div>
<div class="text-xs text-gray-500">Regions</div>
</div>
</div>
<% if (plan === 'free') { %>
<div class="mt-4 pt-4 border-t divider">
<a href="/dashboard/checkout" class="btn-primary inline-flex items-center gap-2 px-4 py-2 text-sm">Upgrade to Pro</a>
</div>
<% } else if (plan === 'pro' && it.account.plan_expires_at) { %>
<div class="mt-4 pt-4 border-t divider text-xs text-gray-500">
Pro plan expires <%= new Date(it.account.plan_expires_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) %>.
<a href="/dashboard/checkout" class="text-blue-400 hover:text-blue-300 ml-1">Extend or upgrade to Lifetime</a>
</div>
<% } %>
</section>
<!-- Account info -->
<section class="card-static p-6">
<h2 class="text-sm font-semibold text-gray-300 mb-4">Account</h2>
<div class="space-y-3">
<% if (!it.isSubKey) { %>
<div>
<label class="block text-xs text-gray-500 mb-1">Login Key</label>
<div class="flex gap-2">
<code id="login-key-display" class="flex-1 bg-surface-solid border border-border-subtle rounded-lg px-4 py-2.5 text-blue-400 text-sm font-mono select-all"><%= it.loginKey %></code>
<button onclick="copyLoginKey()" class="btn-secondary px-3 text-xs">Copy</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 py-2.5 rounded-lg border border-red-900/30 text-red-400 hover:bg-red-900/20 hover:border-red-800/40 text-xs transition-colors">Rotate</button>
</form>
</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"><%= createdDate %></p>
</div>
</div>
</section>
<% if (it.isSubKey) { %>
<section class="card-static p-6">
<p class="text-sm font-semibold text-gray-200 mb-1">Signed in with a sub-key</p>
<p class="text-xs text-gray-500 leading-relaxed">Sub-keys can manage monitors but can't access account credentials. Sign in with your main account key to manage your login key, recovery email, and sub-keys.</p>
</section>
<% } %>
<!-- Email (hidden for sub-key sessions) -->
<% if (!it.isSubKey) { %>
<section class="card-static 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>
<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 input-base px-4 py-2.5 text-gray-100 placeholder-gray-600 text-sm">
<button type="submit" id="email-btn" class="btn-primary px-4 text-sm">Save</button>
<% if (hasEmail) { %>
<button type="submit" name="email" value="" class="btn-secondary px-3 text-xs text-gray-500 hover:!text-red-400">Remove</button>
<% } %>
</div>
</form>
<p id="email-status" class="text-xs mt-2 hidden"></p>
</section>
<% } %>
<!-- Sub-keys (hidden for sub-key sessions) -->
<% if (!it.isSubKey) { %>
<section class="card-static p-6">
<div class="flex items-center justify-between mb-4">
<div>
<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>
</div>
<!-- Create key form -->
<form action="/account/keys" method="POST" class="mb-4 p-4 rounded-lg bg-surface border border-border-subtle">
<input type="hidden" name="_form" value="1">
<label class="block text-xs text-gray-500 mb-1.5">New Sub-Key</label>
<div class="flex gap-2">
<input name="label" type="text" placeholder="e.g. ci-pipeline, mobile-app" required
class="flex-1 input-base px-3 py-2 text-gray-100 placeholder-gray-600 text-sm">
<button type="submit" class="btn-primary px-4 text-sm">Create</button>
</div>
</form>
<!-- Sub-keys list -->
<div id="keys-list" class="space-y-2">
<% if (it.apiKeys.length === 0) { %>
<p class="text-xs text-gray-600 italic">No sub-keys yet.</p>
<% } else { %>
<% it.apiKeys.forEach(function(k) { %>
<div class="p-3 rounded-lg bg-surface border border-border-subtle hover:border-border-strong transition-colors">
<div class="flex items-center justify-between mb-2">
<p class="text-sm text-gray-200"><%= k.label %></p>
<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-surface-solid border border-border-subtle rounded-lg px-3 py-2 text-blue-400 text-xs font-mono select-all"><%= k.key %></code>
<button onclick="copyKey(this, '<%= k.key %>')" class="btn-secondary px-3 text-xs">Copy</button>
</div>
<p class="text-xs text-gray-600 mt-1.5">created <%= new Date(k.created_at).toLocaleDateString() %> <%~ k.last_used_at ? '· last used ' + it.timeAgoSSR(k.last_used_at) : '· never used' %></p>
</div>
<% }) %>
<% } %>
</div>
</section>
<% } %>
<!-- Invoices -->
<% if (it.invoices && it.invoices.length > 0) { %>
<section class="card-static p-6">
<h2 class="text-sm font-semibold text-gray-300 mb-4">Invoices</h2>
<div class="space-y-2">
<% it.invoices.forEach(function(inv) {
const statusColors = { paid: 'green', confirming: 'blue', pending: 'yellow', underpaid: 'orange' };
const statusColor = statusColors[inv.status] || 'gray';
const date = new Date(inv.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
const planLabel = inv.plan === 'lifetime' ? 'Lifetime' : `Pro × ${inv.months}mo`;
%>
<div class="flex items-center justify-between p-3 rounded-lg bg-surface border border-border-subtle hover:border-border-strong transition-colors">
<div class="flex items-center gap-3">
<span class="w-2 h-2 rounded-full bg-<%= statusColor %>-500 <%= inv.status !== 'paid' ? 'animate-pulse' : '' %>"></span>
<div>
<span class="text-sm text-gray-200"><%= planLabel %></span>
<span class="text-xs text-gray-600 ml-2">$<%= Number(inv.amount_usd).toFixed(2) %> · <%= inv.coin.toUpperCase() %></span>
</div>
</div>
<div class="flex items-center gap-3">
<span class="text-xs text-gray-500"><%= date %></span>
<% if (inv.status === 'pending' || inv.status === 'underpaid' || inv.status === 'confirming') { %>
<a href="/dashboard/checkout/<%= inv.id %>" class="text-xs text-blue-400 hover:text-blue-300">View</a>
<% } else if (inv.status === 'paid') { %>
<span class="text-xs text-green-500/70">Paid</span>
<a href="/dashboard/checkout/<%= inv.id %>/receipt" target="_blank" class="text-xs text-blue-400 hover:text-blue-300 transition-colors">Receipt</a>
<% } %>
</div>
</div>
<% }) %>
</div>
</section>
<% } %>
</main>
<script>
// JS enhancements — clipboard copy (progressive, not required)
function copyLoginKey() {
const val = document.getElementById('login-key-display').textContent;
navigator.clipboard.writeText(val);
const btn = event.target;
btn.textContent = 'Copied!'; btn.classList.add('text-green-400');
setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('text-green-400'); }, 1500);
}
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);
}
</script>
<%~ include('./partials/foot') %>