f05d5a2746
Completes the ladder — no tier is a placeholder now. Every tier below builds one thing at a time, which is right for someone still finding their words. At the top the constraint is different: these are people running organisations who don't have an afternoon to spend clicking. So one pass produces five social posts across five different angles, three outreach messages (first approach, follow-up, pyramid-scheme answer), an email, and three text ads — all carrying their link, all in the member's own voice profile if they have one. Partial failures are reported rather than hidden: if the engine can't finish a section the pack says which, instead of quietly handing over a short week. API access is real, not a label: a personal key over GET /me, GET /team, POST /generate. Keys are stored hashed and shown exactly once, compared in constant time, and every call goes through the same meters and compliance rules as the web tools. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
204 lines
7.8 KiB
JavaScript
204 lines
7.8 KiB
JavaScript
// Founder Desk client — builds the weekly pack and manages the API key.
|
||
(function () {
|
||
'use strict';
|
||
var $ = function (id) { return document.getElementById(id); };
|
||
function esc(s) {
|
||
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||
});
|
||
}
|
||
|
||
function gate(msg) {
|
||
$('dGate').style.display = 'block';
|
||
$('dGate').innerHTML = msg;
|
||
}
|
||
|
||
function copyBtn(getText) {
|
||
var b = document.createElement('button');
|
||
b.className = 'btn btn-secondary btn-sm d-copy';
|
||
b.textContent = 'Copy';
|
||
b.addEventListener('click', function () {
|
||
if (navigator.clipboard) navigator.clipboard.writeText(getText());
|
||
b.textContent = 'Copied';
|
||
setTimeout(function () { b.textContent = 'Copy'; }, 1400);
|
||
});
|
||
return b;
|
||
}
|
||
|
||
function item(tag, subject, body) {
|
||
var d = document.createElement('div');
|
||
d.className = 'd-item';
|
||
var html = '<div class="tag">' + esc(tag) + '</div>';
|
||
if (subject) html += '<div class="subj">' + esc(subject) + '</div>';
|
||
html += '<div class="body">' + esc(body) + '</div>';
|
||
d.innerHTML = html;
|
||
d.appendChild(copyBtn(function () { return (subject ? subject + '\n\n' : '') + body; }));
|
||
return d;
|
||
}
|
||
|
||
function section(title, why) {
|
||
var s = document.createElement('div');
|
||
s.className = 'd-sec';
|
||
s.innerHTML = '<h3>' + esc(title) + '</h3><div class="why">' + esc(why) + '</div>';
|
||
return s;
|
||
}
|
||
|
||
function renderPack(pack) {
|
||
var host = $('dPack');
|
||
host.innerHTML = '';
|
||
if (!pack) return;
|
||
|
||
if ((pack.posts || []).length) {
|
||
var s1 = section('Social posts — one per angle',
|
||
'Post one a day. They deliberately take different angles so the week does not read as the same message five times.');
|
||
pack.posts.forEach(function (p) { s1.appendChild(item(p.angle, '', p.text)); });
|
||
host.appendChild(s1);
|
||
}
|
||
|
||
if ((pack.messages || []).length) {
|
||
var s2 = section('Outreach messages',
|
||
'For the conversations that actually build the team — a first approach, a follow-up for someone gone quiet, and the pyramid-scheme answer.');
|
||
pack.messages.forEach(function (mm) { s2.appendChild(item(mm.label, '', mm.text)); });
|
||
host.appendChild(s2);
|
||
}
|
||
|
||
if ((pack.emails || []).length) {
|
||
var s3 = section('Email for your list', 'Drop straight into your autoresponder.');
|
||
pack.emails.forEach(function (em) { s3.appendChild(item('Email', em.subject, em.body)); });
|
||
host.appendChild(s3);
|
||
}
|
||
|
||
if ((pack.textAds || []).length) {
|
||
var s4 = section('Text ads', 'Ready to run from the Traffic Desk.');
|
||
var grid = document.createElement('div');
|
||
grid.className = 'd-ads';
|
||
pack.textAds.forEach(function (a) {
|
||
var c = document.createElement('div');
|
||
c.className = 'd-ad';
|
||
var h = '<div class="s">' + esc(a.subject) + '</div>';
|
||
(a.lines || []).forEach(function (l) { if (l) h += '<div class="l">' + esc(l) + '</div>'; });
|
||
c.innerHTML = h;
|
||
grid.appendChild(c);
|
||
});
|
||
s4.appendChild(grid);
|
||
host.appendChild(s4);
|
||
}
|
||
|
||
// Be straight about anything the engine could not produce, rather than
|
||
// quietly handing over a short pack.
|
||
if ((pack.problems || []).length) {
|
||
var warn = document.createElement('div');
|
||
warn.className = 'd-item';
|
||
warn.style.borderColor = '#f0a05a';
|
||
warn.innerHTML = '<div class="tag" style="color:#f0a05a">Incomplete</div><div class="body">' +
|
||
'The engine could not finish: ' + esc(pack.problems.join(', ')) +
|
||
'. Everything else above is fine — build again to retry the missing pieces.</div>';
|
||
host.appendChild(warn);
|
||
}
|
||
}
|
||
|
||
function renderKey(info, freshKey) {
|
||
var host = $('dKeyState');
|
||
if (freshKey) {
|
||
host.innerHTML = '<div class="k-meta"><b style="color:var(--gold)">Copy this now — it is shown once and never again.</b></div>' +
|
||
'<div class="k-key" id="dFresh">' + esc(freshKey) + '</div>';
|
||
var b = copyBtn(function () { return freshKey; });
|
||
b.style.position = 'static';
|
||
b.style.marginTop = '8px';
|
||
host.appendChild(b);
|
||
$('dIssue').textContent = 'Replace this key';
|
||
$('dRevoke').style.display = '';
|
||
return;
|
||
}
|
||
if (info && info.exists) {
|
||
host.innerHTML = '<div class="k-meta">A key is active (<b>' + esc(info.hint) + '</b>), created ' +
|
||
esc(String(info.createdAt || '').slice(0, 10)) +
|
||
(info.lastUsedAt ? ', last used ' + esc(String(info.lastUsedAt).slice(0, 10)) : ', never used yet') +
|
||
'. The key itself is stored hashed, so it cannot be shown again — create a new one if you have lost it.</div>';
|
||
$('dIssue').textContent = 'Replace this key';
|
||
$('dRevoke').style.display = '';
|
||
} else {
|
||
host.innerHTML = '<div class="k-meta">No key yet.</div>';
|
||
$('dIssue').textContent = 'Create a key';
|
||
$('dRevoke').style.display = 'none';
|
||
}
|
||
}
|
||
|
||
async function build(btn) {
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spin"></span>Building your week — this takes a minute…';
|
||
$('dErr').style.display = 'none';
|
||
try {
|
||
var r = await fetch('/api/public/suite-founder', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}'
|
||
});
|
||
var d = await r.json();
|
||
if (!r.ok) {
|
||
$('dErr').textContent = d.error || 'That did not build — try again.';
|
||
$('dErr').style.display = 'block';
|
||
} else {
|
||
renderPack(d.pack);
|
||
if (d.meter) $('dMeter').textContent = d.meter.remaining + ' of ' + d.meter.limit + ' packs left this month';
|
||
}
|
||
} catch (e) {
|
||
$('dErr').textContent = 'Connection hiccup — try again.';
|
||
$('dErr').style.display = 'block';
|
||
}
|
||
btn.disabled = false;
|
||
btn.innerHTML = '👑 Build my week';
|
||
}
|
||
|
||
async function issue() {
|
||
if (!window.confirm('Create a new API key? Any existing key stops working immediately.')) return;
|
||
try {
|
||
var r = await fetch('/api/public/suite-founder', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ issueKey: true })
|
||
});
|
||
var d = await r.json();
|
||
if (r.ok) renderKey(d.info, d.key);
|
||
} catch (e) {}
|
||
}
|
||
|
||
async function revoke() {
|
||
if (!window.confirm('Revoke your API key? Anything using it stops working.')) return;
|
||
try {
|
||
var r = await fetch('/api/public/suite-founder', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ revokeKey: true })
|
||
});
|
||
var d = await r.json();
|
||
if (r.ok) renderKey(d.info, null);
|
||
} catch (e) {}
|
||
}
|
||
|
||
async function boot() {
|
||
try {
|
||
var r = await fetch('/api/public/suite-founder');
|
||
if (r.status === 401) { gate('You’re not signed in yet. <a href="/suite" style="color:var(--gold);font-weight:700">Open the Suite</a> and connect the wallet that holds your position, then come back.'); return; }
|
||
if (r.status === 403) {
|
||
var g = await r.json();
|
||
gate((g.error || 'Not open for this position yet.') + ' <a href="/suite" style="color:var(--gold);font-weight:700">See your Suite</a>.');
|
||
return;
|
||
}
|
||
if (!r.ok) return;
|
||
var d = await r.json();
|
||
$('dBody').style.display = 'block';
|
||
renderPack(d.pack);
|
||
renderKey(d.key, null);
|
||
if (d.meter) $('dMeter').textContent = d.meter.remaining + ' of ' + d.meter.limit + ' packs left this month';
|
||
if (d.writerReady === false) {
|
||
$('dGo').disabled = true;
|
||
$('dGo').textContent = 'The writer is warming up';
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
boot();
|
||
$('dGo').addEventListener('click', function () { build(this); });
|
||
$('dIssue').addEventListener('click', issue);
|
||
$('dRevoke').addEventListener('click', revoke);
|
||
});
|
||
})();
|