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>
This commit is contained in:
martbost
2026-08-28 11:09:48 -05:00
parent 85261f2719
commit f05d5a2746
7 changed files with 514 additions and 4 deletions
+77
View File
@@ -0,0 +1,77 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><meta name="description" content="Founder Desk — a whole week of promotion built in one pass, plus API access to the Suite."><title>Founder Desk | The Circle Suite</title><link rel="icon" type="image/png" href="/favicon.png"><link rel="stylesheet" href="/styles.css">
<style>
.dw{max-width:900px;margin:0 auto;padding:24px 20px 70px}
.dw h1{font-size:clamp(26px,4.5vw,36px);margin:6px 0 8px}
.dw .lede{color:var(--muted);line-height:1.6;margin-bottom:18px}
#dGate{display:none;border:2px solid var(--gold);border-radius:12px;padding:16px 20px;margin-bottom:18px;line-height:1.6}
.d-card{background:var(--card,#0d2236);border:1px solid var(--line,#24425d);border-radius:14px;padding:18px 20px;margin-bottom:16px}
.d-card h2{font-size:19px;margin:0 0 4px}
.d-card .sub{color:var(--muted);font-size:13.5px;line-height:1.55;margin-bottom:12px}
.d-row{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:14px}
#dErr{display:none;color:#f0a05a;margin-top:12px;font-size:14px;line-height:1.55}
.d-sec{margin-top:24px}
.d-sec > h3{font-size:16px;margin:0 0 4px;color:var(--gold);letter-spacing:.3px}
.d-sec > .why{color:var(--muted);font-size:12.5px;margin-bottom:10px;line-height:1.5}
.d-item{border:1px solid var(--line);border-radius:12px;padding:13px 15px;margin-bottom:10px;position:relative}
.d-item .tag{font-size:10.5px;font-weight:900;letter-spacing:1px;text-transform:uppercase;color:var(--teal);margin-bottom:6px}
.d-item .body{white-space:pre-wrap;line-height:1.65;font-size:14.5px}
.d-item .subj{font-weight:800;margin-bottom:5px}
.d-copy{position:absolute;top:11px;right:11px}
.d-ads{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:10px}
.d-ad{border:1px solid var(--line);border-radius:10px;padding:11px 12px;background:var(--bg,#0b1d2e)}
.d-ad .s{font-weight:800;font-size:15px;color:var(--gold);margin-bottom:4px;word-break:break-word}
.d-ad .l{font-size:13px;line-height:1.5;word-break:break-word}
.k-key{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;background:var(--bg,#0b1d2e);
border:1px solid var(--gold);border-radius:8px;padding:10px 12px;word-break:break-all;margin-top:10px}
.k-meta{color:var(--muted);font-size:12.5px;margin-top:8px;line-height:1.55}
pre.k-doc{background:var(--bg,#0b1d2e);border:1px solid var(--line);border-radius:10px;padding:12px 14px;
overflow-x:auto;font-size:12.5px;line-height:1.6;margin-top:10px}
.spin{display:inline-block;width:13px;height:13px;border:2px solid rgba(255,255,255,.25);border-top-color:var(--gold);border-radius:50%;animation:sp .8s linear infinite;vertical-align:-2px;margin-right:7px}
@keyframes sp{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){.spin{animation:none}}
.d-note{margin-top:22px;font-size:12.5px;color:var(--muted);line-height:1.6;border-top:1px solid var(--line);padding-top:12px}
</style></head>
<body>
<header class="wrap nav"><a class="brand" href="/suite"><img class="brand-mark" src="/logo.jpg" alt="The RM Circle" width="42" height="42"><span><span id="brandName">RM Circle</span><small>Founder Desk</small></span></a><div class="nav-actions"><a class="btn btn-secondary hide-mobile" href="/suite">← The Suite</a><a class="btn btn-primary" href="/my">My Dashboard</a></div></header>
<main class="dw">
<div class="eyebrow" style="color:var(--gold)">CIRCLE SUITE · CORONA (LEVEL 8)</div>
<h1>Founder <span class="gold">Desk.</span></h1>
<p class="lede">Every tool below this builds one thing at a time, which is right when you are still finding your words. At this level the problem is different: you are running an organisation and you do not have an afternoon to spend clicking. So this builds the whole week at once.</p>
<div id="dGate"></div>
<div id="dBody" style="display:none">
<div class="d-card">
<h2>This week's campaign pack</h2>
<div class="sub">Five social posts across five different angles, three outreach messages, an email for your list, and three text ads ready for the Traffic Desk — all carrying your link, all in one pass.</div>
<div class="d-row">
<button id="dGo" class="btn btn-primary">👑 Build my week</button>
<span class="k-meta" id="dMeter" style="margin:0"></span>
</div>
<div id="dErr"></div>
</div>
<div id="dPack"></div>
<div class="d-card" style="margin-top:24px">
<h2>API access</h2>
<div class="sub">A personal key for pulling your position, your organisation triage, and the writer into whatever you already run.</div>
<div id="dKeyState"></div>
<div class="d-row">
<button id="dIssue" class="btn btn-secondary">Create a key</button>
<button id="dRevoke" class="btn btn-secondary" style="display:none">Revoke it</button>
</div>
<pre class="k-doc">GET /api/suite/v1/me your position, level and link
GET /api/suite/v1/team your organisation, triaged
POST /api/suite/v1/generate {"kind":"post","brief":"..."}
curl -H "Authorization: Bearer YOUR_KEY" \
https://rmcircle.team/api/suite/v1/me</pre>
</div>
</div>
<p class="d-note"><strong style="color:var(--text)">Everything here obeys the same rules as the rest of the Suite</strong> — no income claims, no invented figures, POL quantities only, Premium only. Generating a week does not post anything anywhere; it hands you the words and you decide what goes out. <br><br>Independent team resource · No income is guaranteed · Cryptocurrency involves risk.</p>
</main>
<script src="/suite-founder.js"></script>
<script src="/chat.js" defer></script>
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
+203
View File
@@ -0,0 +1,203 @@
// 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);
});
})();
+1 -1
View File
@@ -22,7 +22,7 @@
{ lv: 1, ico: '🚦', name: 'Traffic Desk', desc: 'Syndicated network display advertising — run banners or AI-written text ads on the team’s own ad network. Monthly impressions scale with your level: 2,500 at Scintilla up to 150,000 at Corona.', href: '/suite/traffic', live: true },
{ lv: 6, ico: '🏭', name: 'Funnel Factory', desc: 'A separate hosted landing page for every audience you talk to — each on its own address, each one something you can point a different ad at.', href: '/suite/funnel', live: true },
{ lv: 7, ico: '🧭', name: 'Leader Ops', desc: 'Everyone below you, read live from the contract and sorted by who needs you most — plus a written coaching plan you can teach forward to your own two.', href: '/suite/leader', live: true },
{ lv: 8, ico: '👑', name: 'Founder Desk', desc: 'Every tool in the Suite at its highest allowance, and the top of the ladder — nothing above this to unlock.', live: false }
{ lv: 8, ico: '👑', name: 'Founder Desk', desc: 'A whole week of promotion built in one pass — posts, outreach messages, an email and text ads — plus an API key to pull your position, your organisation and the writer into anything you already run.', href: '/suite/founder', live: true }
];
var $ = function (id) { return document.getElementById(id); };