diff --git a/sites/shkor-com/test_account_flow.py b/sites/shkor-com/test_account_flow.py index 521c055..1f781ba 100644 --- a/sites/shkor-com/test_account_flow.py +++ b/sites/shkor-com/test_account_flow.py @@ -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: diff --git a/sites/shkor-com/v2/account/index.html b/sites/shkor-com/v2/account/index.html index bf65af7..6924653 100644 --- a/sites/shkor-com/v2/account/index.html +++ b/sites/shkor-com/v2/account/index.html @@ -109,44 +109,52 @@ function render() { var d = S, c = d.cabinet, first = (d.name || '').split(' ')[0]; document.getElementById('logout').hidden = false; - var h = '

Your account

Hi, ' + esc(first) + ' Level ' + (c ? c.level : 1) + (c && c.level === 2 ? ' · Studio builder' : ' · Community') + '

'; + var LV = { 1: 'Community', 2: 'Studio builder', 3: 'Studio partner' }; + var h = '

Your account

Hi, ' + esc(first) + ' Level ' + (c ? c.level : 1) + ' · ' + LV[c ? c.level : 1] + '

'; 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 += '

Getting started

' + esc(msg) + '

Open setup status' + ARROW + '
'; app.innerHTML = h; return; } // workspace - if (d.panel) h += '

Your workspace

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.

Panel: ' + esc(d.panel.url) + '
Login: ' + esc(d.panel.user) + ' · Password: ' + esc(d.panel.password) + '
' + if (d.panel) h += '

Your workspace

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.

Panel: ' + esc(d.panel.url) + '
Login: ' + esc(d.panel.user) + ' · Password: ' + esc(d.panel.password) + '
' + 'Open my workspace' + ARROW + '
'; // earnings var e = c.earnings || {}; h += '

Earnings ' + money(e.available) + ' available

Sales through the cooperative’s payments. The studio share is withheld automatically at each payment.

' + '
Sales: ' + money(e.gross) + ' · Studio share: ' + money(e.studio_share) + ' · Yours: ' + money(e.net) + ' · Paid out: ' + money(e.paid_out) + '
' + '

Withdrawals are coming soon — your balance is already tracked.

'; - // 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 += '

' + (c.level === 2 ? 'You’re a Studio builder' : 'Next: Level 2 · Studio builder') + '

' - + '

Level 2 gives you:

' - + '
Claude Code on the studioA coding-agent seat paid by the studio, for your project.
' - + '
Lab agentsAsk the studio’s AI agents for help — research, sales, content.
' - + '
Studio networkContacts, bookings and introductions from the cooperative.
' - + '
Investment poolApply for the quarterly pool — investors vote where it goes.
' - + '

And next, Level 3: a paid role in the studio — by invitation of the board.

'; - // 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 Claude Code — 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 ? '
' + esc(cl.detail) + '' : '') + : 'A Claude Code account from the studio, connected to your workspace for you.'; + h += '

' + (agr ? 'Level 2 · Studio builder Active' : 'Level 2 · Studio builder — one step away') + '

' + + '

' + (agr ? 'Your perks are on.' : 'Sign the contractor agreement and these switch on right away:') + '

' + + '
Claude Code on the studio ' + esc(CL[1]) + '' + clBody + '
' + + '
Live payments ' + (agr ? 'Unlocked' : 'Locked') + 'Request real card payments for your products — switched on after a quick review.
' + + '
'; + 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 ? '

Version ' + esc(agr.version) + '. Read the agreement.

' : '

Read the agreement (' + esc(c.agreement_version) + ') — 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.

' + '
' + '' - + '

', firstOpen === 'agreement'); + + '

', !agr); + h += '
'; + // ── Level 3 · Studio partner: project, live product, traction + var firstOpen = agr ? ['project', 'product', 'traction'].filter(function (k) { return !st[k]; })[0] : null; + h += '

' + (c.level === 3 ? 'Level 3 · Studio partner Active' : 'Next: Level 3 · Studio partner') + '

' + + '

Build a business with the studio behind you:

' + + '
Lab agentsThe studio’s AI agents help with research, sales and content.
' + + '
Studio networkContacts, bookings and introductions from the cooperative.
' + + '
Investment poolApply for the quarterly pool — investors vote where it goes.
' + + '

Beyond that: a paid role in the studio — by invitation of the board.

'; // 3 project var p = c.project || { decisions: {} }, decs = ''; DEC.forEach(function (x) { decs += '
'; }); - 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.', '
' + '
Min ' + c.min_studio_pct + '%. Withheld from each payment through the studio.
' + '
' @@ -162,15 +170,15 @@ : '

') + '
'; }); - 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 + '
' + '
' + '
' + '

Starts in test mode: the link works, but no money moves until live payments are approved.

' - : '

Create your project first (step 3).

'), firstOpen === 'product'); + : '

Create your project first (step 1).

'), 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.', '

Sales: ' + money(e.gross) + ' of ' + money(th.gross_usd) + '

' + '

Payments: ' + (e.payments || 0) + ' of ' + th.payments + '

', firstOpen === 'traction'); h += '
'; @@ -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.' : '');