- Embeddable vanilla-JS chat widget (one script tag + tenant public key):
WebSocket, resumable sessions (24h localStorage token), honeypot + per-IP
and per-message rate limits, Bosnian UI (§7)
- Session auth (bcrypt + signed cookies), owner accounts + super-admins,
admin impersonation ('otvori kao salon', §11)
- Owner dashboard (§10, Bosnian): requests with same actions as email,
call/chat history with transcripts, usage bar; settings for profile,
working hours (manual + LLM paste-to-parse), services CRUD + 'Zalijepi
cjenovnik' LLM import with review step, scheduling (Google OAuth free/busy
read-only + calendar mappings + buffers, partner status), telephony
(forwarding MMI codes per operator, ring group, worker SIP creds + QR),
agent voice/price-mode/notes + widget snippet, notifications/SMS templates
- Super-admin panel (§11): tenant CRUD incl. plan/minutes/paid-until,
partner API config (encrypted secrets), number/SIM registry + assignment,
connectivity test (live availability + dry-run push), versioned prompt
template editor with publish/rollback, playground, usage overview, health
- Fixes: WAL + busy_timeout for sqlite tests, no awaits in WS disconnect
path (deadlock), template publish autoflush bug
- 76 tests green (incl. widget WS e2e booking flow); dashboard, widget and
admin verified live in a browser against Postgres
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
74 lines
4.3 KiB
HTML
74 lines
4.3 KiB
HTML
<!doctype html>
|
|
<html lang="bs">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{% block title %}Gogo Telefon{% endblock %}</title>
|
|
<style>
|
|
:root{--brand:#7c3aed;--brand-dark:#6d28d9;--ok:#16a34a;--warn:#d97706;--err:#dc2626;
|
|
--bg:#f4f4f5;--card:#fff;--line:#e4e4e7;--muted:#71717a;--text:#18181b}
|
|
*{box-sizing:border-box}
|
|
body{font-family:system-ui,sans-serif;margin:0;background:var(--bg);color:var(--text)}
|
|
a{color:var(--brand)}
|
|
header{background:#1e1b2e;color:#fff;padding:0 20px;display:flex;align-items:center;gap:24px;flex-wrap:wrap}
|
|
header .logo{font-weight:800;font-size:17px;padding:14px 0}
|
|
header .logo span{color:#a78bfa}
|
|
header nav{display:flex;gap:2px;flex-wrap:wrap}
|
|
header nav a{color:#c4b5fd;text-decoration:none;padding:16px 12px;font-size:14px}
|
|
header nav a.active,header nav a:hover{color:#fff;border-bottom:3px solid var(--brand)}
|
|
header .right{margin-left:auto;font-size:13px;color:#a1a1aa;padding:14px 0}
|
|
header .right a{color:#a1a1aa}
|
|
main{max-width:1080px;margin:24px auto;padding:0 16px}
|
|
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:20px 24px;margin-bottom:20px}
|
|
.card h2{margin-top:0;font-size:17px}
|
|
h1{font-size:22px}
|
|
table{width:100%;border-collapse:collapse;font-size:14px}
|
|
th{color:var(--muted);text-align:left;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.03em}
|
|
th,td{padding:9px 10px;border-bottom:1px solid var(--line);vertical-align:top}
|
|
tr:last-child td{border-bottom:0}
|
|
input[type=text],input[type=email],input[type=password],input[type=number],input[type=time],input[type=url],select,textarea{
|
|
width:100%;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font:inherit;background:#fff}
|
|
textarea{min-height:90px}
|
|
label{display:block;font-size:13px;font-weight:600;margin:12px 0 4px}
|
|
label small{font-weight:400;color:var(--muted)}
|
|
button,.btn{background:var(--brand);color:#fff;border:0;border-radius:8px;padding:9px 16px;
|
|
font:inherit;font-weight:600;cursor:pointer;text-decoration:none;display:inline-block;font-size:14px}
|
|
button:hover,.btn:hover{background:var(--brand-dark)}
|
|
button.green,.btn.green{background:var(--ok)} button.red,.btn.red{background:var(--err)}
|
|
button.ghost,.btn.ghost{background:transparent;color:var(--brand);border:1px solid var(--brand)}
|
|
button.small,.btn.small{padding:5px 10px;font-size:13px;font-weight:500}
|
|
.badge{display:inline-block;border-radius:20px;padding:2px 10px;font-size:12px;font-weight:600}
|
|
.badge.pending{background:#fef3c7;color:#92400e}
|
|
.badge.resolved_by_owner,.badge.confirmed,.badge.human_answered,.badge.request_created{background:#dcfce7;color:#166534}
|
|
.badge.rejected,.badge.expired,.badge.abandoned{background:#fee2e2;color:#991b1b}
|
|
.badge.info_only,.badge.message_taken{background:#e0e7ff;color:#3730a3}
|
|
.msg{background:#ecfdf5;border:1px solid #a7f3d0;color:#065f46;border-radius:8px;padding:10px 14px;margin-bottom:16px}
|
|
.msg.error{background:#fef2f2;border-color:#fecaca;color:#991b1b}
|
|
.banner{background:#fffbeb;border:1px solid #fde68a;color:#92400e;border-radius:8px;padding:10px 14px;margin-bottom:16px}
|
|
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
|
.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
|
|
@media(max-width:720px){.grid2,.grid3{grid-template-columns:1fr}}
|
|
.muted{color:var(--muted);font-size:13px}
|
|
.bar{background:var(--line);border-radius:10px;height:14px;overflow:hidden}
|
|
.bar>div{height:100%;background:var(--brand)}
|
|
.bar>div.warn{background:var(--warn)} .bar>div.over{background:var(--err)}
|
|
code,pre{background:#f4f4f5;border:1px solid var(--line);border-radius:6px;font-size:13px}
|
|
pre{padding:12px;overflow-x:auto}
|
|
code{padding:1px 6px}
|
|
.transcript{display:flex;flex-direction:column;gap:8px}
|
|
.transcript .t{max-width:75%;padding:8px 12px;border-radius:12px;font-size:14px;white-space:pre-wrap}
|
|
.transcript .t.assistant{background:#ede9fe;align-self:flex-start}
|
|
.transcript .t.user{background:#e4e4e7;align-self:flex-end}
|
|
.transcript .tools{font-size:12px;color:var(--muted);align-self:flex-start}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
{% block nav %}{% endblock %}
|
|
<main>
|
|
{% if request.query_params.get('msg') %}<div class="msg">{{ request.query_params.get('msg') }}</div>{% endif %}
|
|
{% if request.query_params.get('err') %}<div class="msg error">{{ request.query_params.get('err') }}</div>{% endif %}
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
</body>
|
|
</html>
|