Files
martbost f05d5a2746 Founder Desk (L8): weekly campaign pack + Suite API
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>
2026-08-28 11:09:48 -05:00

204 lines
7.8 KiB
JavaScript
Raw Permalink 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.
// 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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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);
});
})();