Compare commits

..

No commits in common. "3d8b743d7a311c24a9254d8de5bcf9ca9f8521b7" and "b0502af84214f010b6b5508f04bf4ff2d4075bfa" have entirely different histories.

2 changed files with 44 additions and 4 deletions

View file

@ -10,7 +10,7 @@
POST /api/account/login JSON email -> magic link by email (only for existing orders) POST /api/account/login JSON email -> magic link by email (only for existing orders)
GET /api/account/session?k=|t= -> session cookie (30 days) -> redirect /account/ GET /api/account/session?k=|t= -> session cookie (30 days) -> redirect /account/
GET /api/account/me cabinet: order status, workspace, level ladder, project, products, earnings GET /api/account/me cabinet: order status, workspace, level ladder, project, products, earnings
POST /api/account/{agreement,project,product,product-live,logout} POST /api/account/{agreement,project,product,product-live,chat,logout}
Env: Env:
SHKOR_DB sqlite path (default /var/lib/shkor-subscribe/subs.db) SHKOR_DB sqlite path (default /var/lib/shkor-subscribe/subs.db)
@ -479,6 +479,8 @@ def acc_action(email, action, d, ip):
elif action == "product": elif action == "product":
body.update(name=clean(d, "name", 120), price_usd=d.get("price_usd"), interval=clean(d, "interval", 12), body.update(name=clean(d, "name", 120), price_usd=d.get("price_usd"), interval=clean(d, "interval", 12),
description=clean(d, "description", 300)) description=clean(d, "description", 300))
elif action == "chat":
body.update(text=str(d.get("text") or "").strip()[:8000])
elif action == "product-live": elif action == "product-live":
body.update(product_id=d.get("product_id"), note=clean(d, "note", 1000)) body.update(product_id=d.get("product_id"), note=clean(d, "note", 1000))
else: else:
@ -592,7 +594,7 @@ class H(BaseHTTPRequestHandler):
email = acc_email(self.sid()) email = acc_email(self.sid())
if not email: if not email:
return self.reply(401, {"error": "Please sign in."}) return self.reply(401, {"error": "Please sign in."})
if limited("acc", self.ip(), 120): if limited("chat" if action == "chat" else "acc", self.ip(), 1500 if action == "chat" else 120):
return self.reply(429, {"error": "Too many requests — slow down a little."}) return self.reply(429, {"error": "Too many requests — slow down a little."})
return self.reply(*acc_action(email, action, d, self.ip())) return self.reply(*acc_action(email, action, d, self.ip()))
if path in ("/api/autopilot/start", "/api/autopilot/checkout"): if path in ("/api/autopilot/start", "/api/autopilot/checkout"):

View file

@ -42,6 +42,13 @@
.ok-msg{color:var(--acid);font-size:14px;min-height:20px;margin-top:10px} .ok-msg{color:var(--acid);font-size:14px;min-height:20px;margin-top:10px}
.muted{color:var(--fg-3);font-size:14px} .muted{color:var(--fg-3);font-size:14px}
.topbar{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap} .topbar{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
.chat{display:flex;flex-direction:column;gap:10px;max-height:420px;overflow-y:auto;margin-top:14px;padding:14px;border-radius:14px;background:rgba(0,0,0,.25);box-shadow:inset 0 0 0 1px var(--line-2)}
.msg{max-width:82%;padding:10px 14px;border-radius:14px;font-size:15px;line-height:1.5;white-space:pre-wrap;word-wrap:break-word;color:var(--fg)}
.msg.me{align-self:flex-end;background:rgba(198,255,77,.14);box-shadow:inset 0 0 0 1px rgba(198,255,77,.3)}
.msg.ag{align-self:flex-start;background:rgba(255,255,255,.05);box-shadow:inset 0 0 0 1px var(--line-2)}
.msg small{display:block;margin-top:4px;font:11px var(--mono);color:var(--fg-3)}
.chat-in{display:flex;gap:10px;margin-top:12px}.chat-in textarea{flex:1;min-height:50px;max-height:160px;resize:vertical;padding:12px 14px;border:0;border-radius:12px;color:var(--fg);font:15px/1.5 var(--body);background:rgba(255,255,255,.035);box-shadow:inset 0 0 0 1px var(--line-2)}
.chat-in textarea:focus{outline:none;box-shadow:inset 0 0 0 1px var(--acid)}
</style> </style>
</head> </head>
<body> <body>
@ -116,8 +123,11 @@
app.innerHTML = h; return; app.innerHTML = h; return;
} }
// workspace // 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>Claude Code (your own Claude account) and OpenCode, both connected to the lab MCP.</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></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>'; // chat with the agent (same channel as the portal chat; the agent answers within ~1 min)
h += '<div class="panel"><h2>Chat with your agent</h2><p>Your Simbiot agent runs on your server around the clock. Ask it anything or give it a task — it usually replies within a minute.</p>'
+ '<div class="chat" id="chat"><p class="muted">Loading…</p></div>'
+ '<form class="chat-in" id="cf"><textarea id="ct" maxlength="8000" placeholder="Message your agent… (Enter to send, Shift+Enter for a new line)"></textarea><button class="btn" type="submit"><span>Send</span></button></form><p class="form-err" id="cerr"></p></div>';
// earnings // earnings
var e = c.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>' 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>'
@ -178,6 +188,28 @@
bind(); bind();
} }
var chatSig = '';
function drawChat(r) {
var box = document.getElementById('chat'); if (!box) return;
var msgs = (r && r.messages) || [], sig = msgs.length + ':' + (msgs.length ? msgs[msgs.length - 1].id : '');
if (sig === chatSig) return; chatSig = sig;
var atBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 40;
box.innerHTML = msgs.length ? msgs.map(function (m) {
var t = new Date(m.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
return '<div class="msg ' + (m.from === 'agent' ? 'ag' : 'me') + '">' + esc(m.text) + '<small>' + (m.from === 'agent' ? 'Agent' : 'You') + ' · ' + t + (m.from !== 'agent' && !m.seen ? ' · sent' : '') + '</small></div>';
}).join('') : '<p class="muted">No messages yet — say hi to your agent.</p>';
if (atBottom || msgs.length && msgs[msgs.length - 1].from !== 'agent') box.scrollTop = box.scrollHeight;
}
function chat(text) {
return api('/api/account/chat', text ? { text: text } : {}).then(function (r) {
if (r.status === 401) return loginView('Your session expired — sign in again.');
var err = document.getElementById('cerr');
if (!r.ok) { if (err) err.textContent = r.j.error || 'Chat is unavailable right now.'; return; }
if (err) err.textContent = ''; drawChat(r.j);
});
}
setInterval(function () { if (!document.hidden && document.getElementById('chat')) chat(''); }, 5000);
function act(path, body, form) { function act(path, body, form) {
var err = form.querySelector('.form-err'), btn = form.querySelector('button[type=submit]'); var err = form.querySelector('.form-err'), btn = form.querySelector('button[type=submit]');
err.textContent = ''; btn.disabled = true; err.textContent = ''; btn.disabled = true;
@ -201,6 +233,12 @@
var fr = document.getElementById('fr'); var fr = document.getElementById('fr');
if (fr) fr.addEventListener('submit', function (e) { e.preventDefault(); act('product', { name: document.getElementById('rn').value.trim(), price_usd: Number(document.getElementById('rp').value), interval: document.getElementById('ri').value }, fr); }); if (fr) fr.addEventListener('submit', function (e) { e.preventDefault(); act('product', { name: document.getElementById('rn').value.trim(), price_usd: Number(document.getElementById('rp').value), interval: document.getElementById('ri').value }, fr); });
document.querySelectorAll('.fl').forEach(function (f) { f.addEventListener('submit', function (e) { e.preventDefault(); act('product-live', { product_id: Number(f.dataset.id), note: f.querySelector('.fl-note').value.trim() }, f); }); }); document.querySelectorAll('.fl').forEach(function (f) { f.addEventListener('submit', function (e) { e.preventDefault(); act('product-live', { product_id: Number(f.dataset.id), note: f.querySelector('.fl-note').value.trim() }, f); }); });
var cf = document.getElementById('cf'), ct = document.getElementById('ct');
if (cf) {
chatSig = ''; chat('');
cf.addEventListener('submit', function (e) { e.preventDefault(); var t = ct.value.trim(); if (!t) return; ct.value = ''; chat(t); });
ct.addEventListener('keydown', function (e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); cf.requestSubmit(); } });
}
document.querySelectorAll('[data-copy]').forEach(function (b) { b.addEventListener('click', function () { navigator.clipboard && navigator.clipboard.writeText(b.dataset.copy); b.textContent = 'Copied'; }); }); document.querySelectorAll('[data-copy]').forEach(function (b) { b.addEventListener('click', function () { navigator.clipboard && navigator.clipboard.writeText(b.dataset.copy); b.textContent = 'Copied'; }); });
} }