209 lines
11 KiB
Plaintext
209 lines
11 KiB
Plaintext
<%~ 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') %>
|