From b0502af84214f010b6b5508f04bf4ff2d4075bfa Mon Sep 17 00:00:00 2001 From: Simbiot Harness Date: Sun, 18 Oct 2026 19:57:19 +0000 Subject: [PATCH] =?UTF-8?q?shkor.com/account:=20=D1=87=D0=B0=D1=82=20?= =?UTF-8?q?=D1=81=20=D0=B0=D0=B3=D0=B5=D0=BD=D1=82=D0=BE=D0=BC=20(=D0=BA?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=D0=BB=20=D0=BF=D0=BE=D1=80=D1=82=D0=B0=D0=BB?= =?UTF-8?q?=D0=B0,=20=D0=BE=D0=BF=D1=80=D0=BE=D1=81=205=20=D1=81,=20=D0=BE?= =?UTF-8?q?=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=BB=D0=B8?= =?UTF-8?q?=D0=BC=D0=B8=D1=82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- sites/shkor-com/api.py | 6 +++-- sites/shkor-com/v2/account/index.html | 39 +++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/sites/shkor-com/api.py b/sites/shkor-com/api.py index 60b0bdc..5355721 100644 --- a/sites/shkor-com/api.py +++ b/sites/shkor-com/api.py @@ -10,7 +10,7 @@ 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/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: 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": body.update(name=clean(d, "name", 120), price_usd=d.get("price_usd"), interval=clean(d, "interval", 12), description=clean(d, "description", 300)) + elif action == "chat": + body.update(text=str(d.get("text") or "").strip()[:8000]) elif action == "product-live": body.update(product_id=d.get("product_id"), note=clean(d, "note", 1000)) else: @@ -592,7 +594,7 @@ class H(BaseHTTPRequestHandler): email = acc_email(self.sid()) if not email: 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(*acc_action(email, action, d, self.ip())) if path in ("/api/autopilot/start", "/api/autopilot/checkout"): diff --git a/sites/shkor-com/v2/account/index.html b/sites/shkor-com/v2/account/index.html index 7ccd3db..c0f4c6b 100644 --- a/sites/shkor-com/v2/account/index.html +++ b/sites/shkor-com/v2/account/index.html @@ -42,6 +42,13 @@ .ok-msg{color:var(--acid);font-size:14px;min-height:20px;margin-top:10px} .muted{color:var(--fg-3);font-size:14px} .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)} @@ -117,6 +124,10 @@ } // workspace if (d.panel) h += '

Your workspace

Claude Code (your own Claude account) and OpenCode, both connected to the lab MCP.

Panel: ' + esc(d.panel.url) + '
Login: ' + esc(d.panel.user) + ' · Password: ' + esc(d.panel.password) + '
'; + // chat with the agent (same channel as the portal chat; the agent answers within ~1 min) + h += '

Chat with your agent

Your Simbiot agent runs on your server around the clock. Ask it anything or give it a task — it usually replies within a minute.

' + + '

Loading…

' + + '

'; // 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.

' @@ -177,6 +188,28 @@ 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 '
' + esc(m.text) + '' + (m.from === 'agent' ? 'Agent' : 'You') + ' · ' + t + (m.from !== 'agent' && !m.seen ? ' · sent' : '') + '
'; + }).join('') : '

No messages yet — say hi to your agent.

'; + 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) { var err = form.querySelector('.form-err'), btn = form.querySelector('button[type=submit]'); err.textContent = ''; btn.disabled = true; @@ -200,6 +233,12 @@ 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); }); 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'; }); }); }