shkor.com/account: уровень 2 — одна подпись договора, перки со статусом Claude Code (Unlocks / Setting up / Connected) и live-платежи; проект/продукт/трекшн — уровень 3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
578856081e
commit
110193a698
2 changed files with 38 additions and 22 deletions
|
|
@ -48,6 +48,8 @@ try:
|
|||
c,_,b=req("/api/account/agreement",{"signer_name":"Someone Else","accept":True},C); eq(c==400,"name must match")
|
||||
c,_,b=req("/api/account/agreement",{"signer_name":"cab tester","accept":False},C); eq(c==400,"must accept "+b.decode()[:100])
|
||||
c,_,b=req("/api/account/agreement",{"signer_name":"Cab Tester","accept":True},C); r=json.loads(b); eq(c==200 and r.get("agreement"),"signed "+b.decode()[:200])
|
||||
eq(r.get("level")==2,"level 2 right after signing")
|
||||
eq((r.get("claude") or {}).get("status")=="requested","claude requested after signing: "+str(r.get("claude")))
|
||||
# product before project
|
||||
c,_,b=req("/api/account/product",{"name":"Pro","price_usd":29,"interval":"month"},C); eq(c==409,"product needs project")
|
||||
# project
|
||||
|
|
@ -62,7 +64,7 @@ try:
|
|||
print("link", p.get("link"))
|
||||
c,_,b=req("/api/account/product-live",{"product_id":p["id"],"note":"Monthly access"},C); r=json.loads(b); eq(c==200 and r["products"][0]["live_request"]=="pending","live requested "+b.decode()[:200])
|
||||
c,_,b=req("/api/account/product-live",{"product_id":999999},C); eq(c==404,"foreign product 404")
|
||||
c,_,b=req("/api/account/me",cookie=C); d=json.loads(b); eq(d["cabinet"]["level"]==1 and d["cabinet"]["steps"][3]["done"] is False,"still level1 until live")
|
||||
c,_,b=req("/api/account/me",cookie=C); d=json.loads(b); eq(d["cabinet"]["level"]==2 and d["cabinet"]["steps"][3]["done"] is False,"level 2 (not 3) until live product")
|
||||
c,_,b=req("/api/account/logout",{},C); eq(c==200,"logout")
|
||||
print(f"{ok} ok, {bad} fail")
|
||||
finally:
|
||||
|
|
|
|||
|
|
@ -109,44 +109,52 @@
|
|||
function render() {
|
||||
var d = S, c = d.cabinet, first = (d.name || '').split(' ')[0];
|
||||
document.getElementById('logout').hidden = false;
|
||||
var h = '<div class="topbar"><div><p class="kicker">Your account</p><h1>Hi, ' + esc(first) + ' <span class="lvl">Level ' + (c ? c.level : 1) + (c && c.level === 2 ? ' · Studio builder' : ' · Community') + '</span></h1></div></div>';
|
||||
var LV = { 1: 'Community', 2: 'Studio builder', 3: 'Studio partner' };
|
||||
var h = '<div class="topbar"><div><p class="kicker">Your account</p><h1>Hi, ' + esc(first) + ' <span class="lvl">Level ' + (c ? c.level : 1) + ' · ' + LV[c ? c.level : 1] + '</span></h1></div></div>';
|
||||
if (d.stage !== 'ready' || !c) {
|
||||
var msg = { kyc: 'Finish identity verification to continue.', pay: 'Verified — one step left: payment.', declined: 'Identity verification was not approved.', deploying: 'Your workspace is deploying — usually 15–40 minutes.' }[d.stage] || '';
|
||||
h += '<div class="panel"><h2>Getting started</h2><p>' + esc(msg) + '</p><a class="btn" style="margin-top:14px" href="../start/status/?t=' + encodeURIComponent(d.token) + '"><span>Open setup status</span>' + ARROW + '</a></div>';
|
||||
app.innerHTML = h; return;
|
||||
}
|
||||
// workspace
|
||||
if (d.panel) h += '<div class="panel"><h2>Your workspace</h2><p>Your cloud workspace: OpenCode is ready to go, Claude Code works with your own Claude account. Both are connected to the lab MCP — git, subdomains and payments.</p><div class="creds">Panel: <b><a href="' + esc(d.panel.url) + '" target="_blank" rel="noopener">' + esc(d.panel.url) + '</a></b><br>Login: <b>' + esc(d.panel.user) + '</b> · Password: <b>' + esc(d.panel.password) + '</b></div>'
|
||||
if (d.panel) h += '<div class="panel"><h2>Your workspace</h2><p>Your cloud workspace: OpenCode is ready to go; Claude Code ' + ((c.claude || {}).status === 'connected' ? 'runs on the studio (Level 2).' : 'works with your own Claude account — or on the studio from Level 2.') + ' Both are connected to the lab MCP — git, subdomains and payments.</p><div class="creds">Panel: <b><a href="' + esc(d.panel.url) + '" target="_blank" rel="noopener">' + esc(d.panel.url) + '</a></b><br>Login: <b>' + esc(d.panel.user) + '</b> · Password: <b>' + esc(d.panel.password) + '</b></div>'
|
||||
+ '<a class="btn" style="margin-top:14px" href="' + esc(d.panel.url) + '" target="_blank" rel="noopener"><span>Open my workspace</span>' + ARROW + '</a></div>';
|
||||
// earnings
|
||||
var e = c.earnings || {};
|
||||
h += '<div class="panel"><h2>Earnings <span class="pill ' + (e.available > 0 ? 'ok' : 'wip') + '">' + money(e.available) + ' available</span></h2><p>Sales through the cooperative’s payments. The studio share is withheld automatically at each payment.</p>'
|
||||
+ '<div class="creds">Sales: <b>' + money(e.gross) + '</b> · Studio share: <b>' + money(e.studio_share) + '</b> · Yours: <b>' + money(e.net) + '</b> · Paid out: <b>' + money(e.paid_out) + '</b></div>'
|
||||
+ '<p class="muted" style="margin-top:10px">Withdrawals are coming soon — your balance is already tracked.</p></div>';
|
||||
// level 2
|
||||
var st = {}; (c.steps || []).forEach(function (s) { st[s.key] = s.done; });
|
||||
var firstOpen = ['agreement', 'project', 'product', 'traction'].filter(function (k) { return !st[k]; })[0];
|
||||
h += '<div class="panel"><h2>' + (c.level === 2 ? 'You’re a Studio builder' : 'Next: Level 2 · Studio builder') + '</h2>'
|
||||
+ '<p>Level 2 gives you:</p><div class="gives">'
|
||||
+ '<div><b>Claude Code on the studio</b>A coding-agent seat paid by the studio, for your project.</div>'
|
||||
+ '<div><b>Lab agents</b>Ask the studio’s AI agents for help — research, sales, content.</div>'
|
||||
+ '<div><b>Studio network</b>Contacts, bookings and introductions from the cooperative.</div>'
|
||||
+ '<div><b>Investment pool</b>Apply for the quarterly pool — investors vote where it goes.</div></div>'
|
||||
+ '<p class="muted">And next, Level 3: a paid role in the studio — by invitation of the board.</p>';
|
||||
// 1 kyc
|
||||
h += stepHTML(1, 'kyc', true, 'Verify your identity', 'Done with Didit.', '', false);
|
||||
// 2 agreement
|
||||
var agr = c.agreement;
|
||||
h += stepHTML(2, 'agreement', st.agreement, 'Sign the contractor agreement', agr ? 'Signed by ' + esc(agr.signer_name) + ' · ' + esc(agr.signed_at) + ' UTC' : 'You keep your project. The studio gets a share of revenue you set (min ' + c.min_studio_pct + '%).',
|
||||
var agr = c.agreement, cl = c.claude || { status: 'locked' };
|
||||
// ── Level 2 · Studio builder: one step — the contractor agreement
|
||||
var CL = { locked: ['todo', 'Unlocks when you sign'], requested: ['wip', 'Setting up your account…'], connected: ['ok', 'Connected'] }[cl.status] || ['wip', cl.status];
|
||||
var clBody = cl.status === 'connected' ? 'Open your workspace and choose <b>Claude Code</b> — it runs on the studio’s account, nothing to pay or sign in.'
|
||||
: cl.status === 'requested' ? 'The studio is setting up a Claude Code account for you — usually within a day. This turns green by itself once it works on your server.' + (cl.detail ? '<br><span class="muted">' + esc(cl.detail) + '</span>' : '')
|
||||
: 'A Claude Code account from the studio, connected to your workspace for you.';
|
||||
h += '<div class="panel"><h2>' + (agr ? 'Level 2 · Studio builder <span class="pill ok">Active</span>' : 'Level 2 · Studio builder — one step away') + '</h2>'
|
||||
+ '<p>' + (agr ? 'Your perks are on.' : 'Sign the contractor agreement and these switch on right away:') + '</p><div class="gives">'
|
||||
+ '<div><b>Claude Code on the studio <span class="pill ' + CL[0] + '">' + esc(CL[1]) + '</span></b>' + clBody + '</div>'
|
||||
+ '<div><b>Live payments <span class="pill ' + (agr ? 'ok' : 'todo') + '">' + (agr ? 'Unlocked' : 'Locked') + '</span></b>Request real card payments for your products — switched on after a quick review.</div>'
|
||||
+ '</div>';
|
||||
h += stepHTML(1, 'agreement', !!agr, 'Sign the contractor agreement', agr ? 'Signed by ' + esc(agr.signer_name) + ' · ' + esc(agr.signed_at) + ' UTC' : 'You keep your project. The studio gets a share of revenue you set (min ' + c.min_studio_pct + '%).',
|
||||
agr ? '<p class="muted">Version ' + esc(agr.version) + '. <a href="agreement/" target="_blank" rel="noopener">Read the agreement</a>.</p>'
|
||||
: '<p class="muted"><a href="agreement/" target="_blank" rel="noopener">Read the agreement (' + esc(c.agreement_version) + ')</a> — about 3 minutes. Key points: you own your project; the studio share (min ' + c.min_studio_pct + '%) is withheld from payments through the studio; you’re an independent contractor; 30 days’ notice to end.</p>'
|
||||
+ '<form id="fa" novalidate><div class="field"><label for="an">Type your full name as verified: <em>' + esc(d.name) + '</em></label><input id="an" autocomplete="name" maxlength="120"></div>'
|
||||
+ '<label class="check"><input type="checkbox" id="ac"><span>I have read and accept the Contractor Agreement (' + esc(c.agreement_version) + ') and sign it electronically.</span></label>'
|
||||
+ '<button class="btn" style="margin-top:16px" type="submit"><span>Sign agreement</span>' + ARROW + '</button><p class="form-err"></p></form>', firstOpen === 'agreement');
|
||||
+ '<button class="btn" style="margin-top:16px" type="submit"><span>Sign and unlock Level 2</span>' + ARROW + '</button><p class="form-err"></p></form>', !agr);
|
||||
h += '</div>';
|
||||
// ── Level 3 · Studio partner: project, live product, traction
|
||||
var firstOpen = agr ? ['project', 'product', 'traction'].filter(function (k) { return !st[k]; })[0] : null;
|
||||
h += '<div class="panel"><h2>' + (c.level === 3 ? 'Level 3 · Studio partner <span class="pill ok">Active</span>' : 'Next: Level 3 · Studio partner') + '</h2>'
|
||||
+ '<p>Build a business with the studio behind you:</p><div class="gives">'
|
||||
+ '<div><b>Lab agents</b>The studio’s AI agents help with research, sales and content.</div>'
|
||||
+ '<div><b>Studio network</b>Contacts, bookings and introductions from the cooperative.</div>'
|
||||
+ '<div><b>Investment pool</b>Apply for the quarterly pool — investors vote where it goes.</div></div>'
|
||||
+ '<p class="muted">Beyond that: a paid role in the studio — by invitation of the board.</p>';
|
||||
// 3 project
|
||||
var p = c.project || { decisions: {} }, decs = '';
|
||||
DEC.forEach(function (x) { decs += '<div class="field"><label for="p-' + x[0] + '">' + x[1] + ' <em>*</em></label><textarea id="p-' + x[0] + '" maxlength="600" placeholder="' + esc(x[2]) + '">' + esc((p.decisions || {})[x[0]]) + '</textarea></div>'; });
|
||||
h += stepHTML(3, 'project', st.project, 'Describe your project', c.project ? esc(c.project.name) + ' · studio share ' + esc(c.project.studio_pct) + '%' : 'Name, studio share and the 7 decisions every studio project defines.',
|
||||
h += stepHTML(1, 'project', st.project, 'Describe your project', c.project ? esc(c.project.name) + ' · studio share ' + esc(c.project.studio_pct) + '%' : 'Name, studio share and the 7 decisions every studio project defines.',
|
||||
'<form id="fp" novalidate><div class="row2"><div class="field"><label for="pn">Project name <em>*</em></label><input id="pn" maxlength="120" value="' + esc(p.name) + '"' + (c.project ? ' disabled' : '') + '></div>'
|
||||
+ '<div class="field"><label for="pp">Studio share, % <em>*</em></label><input id="pp" type="number" min="' + c.min_studio_pct + '" max="100" step="1" value="' + esc(c.project ? c.project.studio_pct : c.min_studio_pct) + '"' + (c.project ? ' disabled' : '') + '><small>Min ' + c.min_studio_pct + '%. Withheld from each payment through the studio.</small></div></div>'
|
||||
+ '<div class="field"><label for="pd">One-line description</label><input id="pd" maxlength="600" value="' + esc(p.description) + '"' + (c.project ? ' disabled' : '') + '></div>'
|
||||
|
|
@ -162,15 +170,15 @@
|
|||
: '<form class="fl" data-id="' + x.id + '" style="margin-top:12px"><div class="field"><label>What exactly will customers pay for?</label><input class="fl-note" maxlength="1000" placeholder="e.g. Monthly access to … for small clinics"></div><button class="btn btn-sm" type="submit"><span>Request live payments</span></button><p class="form-err"></p></form>')
|
||||
+ '</div>';
|
||||
});
|
||||
h += stepHTML(4, 'product', st.product, 'Launch a product', 'Generate a product with a payment link, share it, then go live after a quick review.',
|
||||
h += stepHTML(2, 'product', st.product, 'Launch a product', 'Generate a product with a payment link, share it, then go live after a quick review.',
|
||||
(c.project ? prods + '<form id="fr" novalidate style="margin-top:12px"><div class="row3"><div class="field"><label for="rn">Product name</label><input id="rn" maxlength="120" placeholder="e.g. Pro plan"></div>'
|
||||
+ '<div class="field"><label for="rp">Price, USD</label><input id="rp" type="number" min="1" max="20000" step="1" value="29"></div>'
|
||||
+ '<div class="field"><label for="ri">Billing</label><select id="ri"><option value="month">Monthly</option><option value="year">Yearly</option><option value="one_time">One-time</option></select></div></div>'
|
||||
+ '<button class="btn" type="submit"><span>Generate product</span>' + ARROW + '</button><p class="form-err"></p><p class="muted">Starts in test mode: the link works, but no money moves until live payments are approved.</p></form>'
|
||||
: '<p class="muted">Create your project first (step 3).</p>'), firstOpen === 'product');
|
||||
: '<p class="muted">Create your project first (step 1).</p>'), firstOpen === 'product');
|
||||
// 5 traction
|
||||
var th = c.thresholds || { gross_usd: 300, payments: 10 }, g = Math.min(100, (e.gross || 0) / th.gross_usd * 100), n = Math.min(100, (e.payments || 0) / th.payments * 100);
|
||||
h += stepHTML(5, 'traction', st.traction, 'Show traction', 'Either ' + money(th.gross_usd) + ' in sales or ' + th.payments + ' paying customers through studio payments.',
|
||||
h += stepHTML(3, 'traction', st.traction, 'Show traction', 'Either ' + money(th.gross_usd) + ' in sales or ' + th.payments + ' paying customers through studio payments.',
|
||||
'<p class="muted">Sales: ' + money(e.gross) + ' of ' + money(th.gross_usd) + '</p><div class="bar"><i style="width:' + g + '%"></i></div>'
|
||||
+ '<p class="muted">Payments: ' + (e.payments || 0) + ' of ' + th.payments + '</p><div class="bar"><i style="width:' + n + '%"></i></div>', firstOpen === 'traction');
|
||||
h += '</div>';
|
||||
|
|
@ -204,6 +212,12 @@
|
|||
document.querySelectorAll('[data-copy]').forEach(function (b) { b.addEventListener('click', function () { navigator.clipboard && navigator.clipboard.writeText(b.dataset.copy); b.textContent = 'Copied'; }); });
|
||||
}
|
||||
|
||||
// пока студия подключает Claude Code — обновлять кабинет раз в минуту
|
||||
setInterval(function () {
|
||||
var cl = S && S.cabinet && S.cabinet.claude;
|
||||
if (!document.hidden && cl && cl.status === 'requested' && !document.querySelector('form :focus')) load();
|
||||
}, 60000);
|
||||
|
||||
function load() {
|
||||
api('/api/account/me').then(function (r) {
|
||||
if (r.status === 401) return loginView(q.get('expired') ? 'That link has expired or was already used — request a new one.' : '');
|
||||
|
|
|
|||
Loading…
Reference in a new issue