leedab 0.1.9 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/resolveMember.d.ts +21 -0
- package/dist/agent/resolveMember.js +44 -0
- package/dist/dashboard/routes.d.ts +12 -0
- package/dist/dashboard/routes.js +306 -26
- package/dist/dashboard/server.js +6 -1
- package/dist/dashboard/static/admin.html +688 -0
- package/dist/dashboard/static/index.html +59 -32
- package/dist/dashboard/static/sessions.html +21 -45
- package/dist/license.d.ts +3 -0
- package/dist/license.js +9 -2
- package/dist/team/permissions.d.ts +65 -0
- package/dist/team/permissions.js +138 -0
- package/dist/team/syncAllowlists.d.ts +7 -0
- package/dist/team/syncAllowlists.js +62 -0
- package/dist/team.d.ts +10 -7
- package/dist/team.js +62 -68
- package/dist/templates/verticals/supply-chain/WORKFLOWS.md +5 -0
- package/dist/workflows/registry.d.ts +22 -0
- package/dist/workflows/registry.js +46 -0
- package/package.json +1 -1
- package/dist/dashboard/static/app.js +0 -351
- package/dist/dashboard/static/console.html +0 -252
- package/dist/dashboard/static/settings.html +0 -274
- package/dist/dashboard/static/team.html +0 -215
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<link rel="icon" type="image/png" href="/favicon.png">
|
|
7
|
+
<title>LeedAB — Admin</title>
|
|
8
|
+
<link rel="stylesheet" href="/style.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<style>
|
|
12
|
+
.page-header { display:flex; align-items:center; justify-content:space-between; padding:0 20px; height:52px; border-bottom:1px solid var(--border); background:var(--bg); }
|
|
13
|
+
.page-header-left { display:flex; align-items:center; gap:10px; }
|
|
14
|
+
.page-header-title { font-size:13px; font-weight:700; letter-spacing:-0.01em; }
|
|
15
|
+
.page-nav { display:flex; align-items:center; gap:2px; }
|
|
16
|
+
.page-nav a, .page-nav button { color:var(--text-dim); text-decoration:none; display:flex; align-items:center; gap:5px; padding:6px 12px; border-radius:8px; font-size:13px; font-weight:450; transition:all 0.15s; background:none; border:none; cursor:pointer; font-family:inherit; }
|
|
17
|
+
.page-nav a:hover, .page-nav button:hover { color:var(--text-secondary); background:var(--surface-raised); }
|
|
18
|
+
.page-nav .theme-btn { border:1px solid var(--border); padding:5px 8px; }
|
|
19
|
+
|
|
20
|
+
.tab-bar { display:flex; gap:4px; padding:12px 20px 0; background:var(--bg); position:sticky; top:0; z-index:10; }
|
|
21
|
+
.tab-bar button { background:none; border:none; color:var(--text-dim); font-family:inherit; font-size:13px; font-weight:500; padding:10px 16px; border-radius:8px; cursor:pointer; transition:all 0.15s; }
|
|
22
|
+
.tab-bar button:hover { color:var(--text-secondary); background:var(--surface-raised); }
|
|
23
|
+
.tab-bar button.active { color:var(--accent); background:var(--accent-soft); }
|
|
24
|
+
|
|
25
|
+
.tab-panel { display:none; }
|
|
26
|
+
.tab-panel.active { display:block; }
|
|
27
|
+
|
|
28
|
+
/* team */
|
|
29
|
+
.team-table { width:100%; border-collapse:collapse; }
|
|
30
|
+
.team-table th, .team-table td { text-align:left; padding:10px 12px; font-size:0.875rem; border-bottom:1px solid var(--border); vertical-align:top; }
|
|
31
|
+
.team-table tbody tr:last-child td { border-bottom:none; }
|
|
32
|
+
.team-table th { color:var(--text-faint); font-weight:500; font-size:0.75rem; text-transform:uppercase; letter-spacing:0.05em; }
|
|
33
|
+
.team-table tr.row { cursor:pointer; }
|
|
34
|
+
.team-table tr.row:hover { background:var(--surface-raised); }
|
|
35
|
+
.role-badge { display:inline-block; padding:2px 8px; border-radius:6px; font-size:11px; font-weight:500; }
|
|
36
|
+
.role-admin { background:var(--accent-soft); color:var(--accent); }
|
|
37
|
+
.role-owner { background:rgba(34, 197, 94, 0.1); color:var(--success); }
|
|
38
|
+
.role-member { background:rgba(113, 113, 122, 0.1); color:var(--text-dim); }
|
|
39
|
+
.chip-group { display:flex; flex-wrap:wrap; gap:4px; }
|
|
40
|
+
.chip { display:inline-block; padding:2px 8px; border-radius:10px; font-size:11px; background:var(--surface-raised); color:var(--text-dim); border:1px solid var(--border); }
|
|
41
|
+
.chip.empty { color:var(--text-faint); font-style:italic; background:transparent; border-style:dashed; }
|
|
42
|
+
.chip.more { color:var(--text-faint); font-size:10px; cursor:default; }
|
|
43
|
+
/* drawer */
|
|
44
|
+
.drawer-backdrop { position:fixed; inset:0; background:rgba(0,0,0,0.4); display:none; z-index:50; }
|
|
45
|
+
.drawer-backdrop.open { display:block; }
|
|
46
|
+
.drawer { position:fixed; top:0; right:0; bottom:0; width:min(440px, 90vw); background:var(--bg); border-left:1px solid var(--border); padding:20px; overflow-y:auto; z-index:60; transform:translateX(100%); transition:transform 0.2s; display:flex; flex-direction:column; gap:16px; }
|
|
47
|
+
.drawer.open { transform:translateX(0); }
|
|
48
|
+
.drawer h3 { margin:0; font-size:15px; }
|
|
49
|
+
.drawer label { font-size:12px; color:var(--text-dim); display:block; }
|
|
50
|
+
.drawer input[type=text], .drawer input[type=email] { width:100%; margin-top:4px; padding:8px 10px; background:var(--bg); border:1px solid var(--border); border-radius:6px; color:var(--text); font-size:13px; font-family:inherit; }
|
|
51
|
+
.drawer .checklist { display:flex; flex-direction:column; gap:6px; max-height:260px; overflow-y:auto; padding:8px; border:1px solid var(--border); border-radius:8px; }
|
|
52
|
+
.drawer .checklist label { display:flex; align-items:flex-start; gap:8px; color:var(--text); font-size:13px; cursor:pointer; }
|
|
53
|
+
.drawer .checklist label small { display:block; color:var(--text-faint); font-size:11px; margin-top:1px; }
|
|
54
|
+
.drawer .drawer-actions { display:flex; gap:8px; justify-content:flex-end; margin-top:auto; border-top:1px solid var(--border); padding-top:12px; }
|
|
55
|
+
</style>
|
|
56
|
+
|
|
57
|
+
<div class="page-header">
|
|
58
|
+
<div class="page-header-left">
|
|
59
|
+
<a href="/" class="page-header-title" style="text-decoration:none;color:inherit">LeedAB</a>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="page-nav">
|
|
62
|
+
<button class="theme-btn" onclick="toggleTheme()" title="Toggle theme">
|
|
63
|
+
<svg id="theme-icon-sun" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
|
64
|
+
<svg id="theme-icon-moon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div class="tab-bar">
|
|
70
|
+
<button id="tab-team" class="active" onclick="switchTab('team')">Team</button>
|
|
71
|
+
<button id="tab-channels" onclick="switchTab('channels')">Channels</button>
|
|
72
|
+
<button id="tab-credentials" onclick="switchTab('credentials')">Credentials</button>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="container">
|
|
76
|
+
<main>
|
|
77
|
+
<div id="toast" class="toast hidden"></div>
|
|
78
|
+
|
|
79
|
+
<!-- TEAM TAB -->
|
|
80
|
+
<div id="panel-team" class="tab-panel active">
|
|
81
|
+
<section class="section">
|
|
82
|
+
<div style="display:flex;align-items:baseline;justify-content:space-between">
|
|
83
|
+
<h2 class="section-title">Members</h2>
|
|
84
|
+
<span id="seat-chip" style="display:none;font-size:12px;color:var(--text-faint)"></span>
|
|
85
|
+
</div>
|
|
86
|
+
<div id="team-list"></div>
|
|
87
|
+
</section>
|
|
88
|
+
|
|
89
|
+
<div id="add-member-row" style="display:flex;align-items:center;gap:8px;padding:8px 12px;flex-wrap:wrap">
|
|
90
|
+
<input type="text" id="member-name" placeholder="Name" style="flex:1;min-width:100px;padding:7px 10px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;font-family:inherit">
|
|
91
|
+
<input type="email" id="member-email" placeholder="Email (optional)" style="flex:1;min-width:140px;padding:7px 10px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;font-family:inherit">
|
|
92
|
+
<select id="member-role" style="padding:7px 10px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;font-family:inherit">
|
|
93
|
+
<option value="member">Member</option>
|
|
94
|
+
<option value="admin">Admin</option>
|
|
95
|
+
</select>
|
|
96
|
+
<button id="invite-btn" class="btn" onclick="addTeamMember()" style="padding:7px 14px;font-size:13px;white-space:nowrap">Add</button>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<!-- CHANNELS TAB -->
|
|
101
|
+
<div id="panel-channels" class="tab-panel">
|
|
102
|
+
<section class="section">
|
|
103
|
+
<h2 class="section-title">Channels</h2>
|
|
104
|
+
<p class="section-desc">How your team reaches the agent.</p>
|
|
105
|
+
|
|
106
|
+
<div class="cards">
|
|
107
|
+
<div class="card" id="card-telegram">
|
|
108
|
+
<div class="card-icon">
|
|
109
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
110
|
+
<path d="m22 2-7 20-4-9-9-4 20-7Z"/><path d="M22 2 11 13"/>
|
|
111
|
+
</svg>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="card-body">
|
|
114
|
+
<h3>Telegram</h3>
|
|
115
|
+
<div class="card-status" id="status-telegram">
|
|
116
|
+
<span class="dot"></span> Not connected
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
<button class="btn" onclick="showTelegramForm()">Connect</button>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="card" id="card-whatsapp">
|
|
123
|
+
<div class="card-icon">
|
|
124
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
125
|
+
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
|
|
126
|
+
</svg>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="card-body">
|
|
129
|
+
<h3>WhatsApp</h3>
|
|
130
|
+
<div class="card-status" id="status-whatsapp">
|
|
131
|
+
<span class="dot"></span> Not connected
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
<button class="btn" onclick="connectWhatsApp()">Connect</button>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div class="card" id="card-teams">
|
|
138
|
+
<div class="card-icon">
|
|
139
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
140
|
+
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/>
|
|
141
|
+
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>
|
|
142
|
+
</svg>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="card-body">
|
|
145
|
+
<h3>Microsoft Teams</h3>
|
|
146
|
+
<div class="card-status" id="status-teams">
|
|
147
|
+
<span class="dot"></span> Not connected
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
<button class="btn" onclick="showTeamsForm()">Connect</button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</section>
|
|
154
|
+
|
|
155
|
+
<div id="telegram-form" class="form-panel hidden">
|
|
156
|
+
<h3>Connect Telegram</h3>
|
|
157
|
+
<p class="form-help">Get a bot token from <a href="https://t.me/BotFather" target="_blank">@BotFather</a> on Telegram.</p>
|
|
158
|
+
<label>Bot Token
|
|
159
|
+
<input type="password" id="telegram-token" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11">
|
|
160
|
+
</label>
|
|
161
|
+
<div class="form-actions">
|
|
162
|
+
<button class="btn" onclick="connectTelegram()">Connect</button>
|
|
163
|
+
<button class="btn btn-ghost" onclick="hideTelegramForm()">Cancel</button>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div id="teams-form" class="form-panel hidden">
|
|
168
|
+
<h3>Connect Microsoft Teams</h3>
|
|
169
|
+
<p class="form-help">Enter your Azure AD app credentials. <a href="https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps" target="_blank">Create one here</a></p>
|
|
170
|
+
<label>App (Client) ID
|
|
171
|
+
<input type="text" id="teams-app-id" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
|
|
172
|
+
</label>
|
|
173
|
+
<label>Tenant ID
|
|
174
|
+
<input type="text" id="teams-tenant-id" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
|
|
175
|
+
</label>
|
|
176
|
+
<div class="form-actions">
|
|
177
|
+
<button class="btn" onclick="connectTeams()">Connect</button>
|
|
178
|
+
<button class="btn btn-ghost" onclick="hideTeamsForm()">Cancel</button>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div id="whatsapp-panel" class="form-panel hidden">
|
|
183
|
+
<h3>WhatsApp Pairing</h3>
|
|
184
|
+
<p class="form-help">Open WhatsApp on your phone → Settings → Linked Devices → Link a Device</p>
|
|
185
|
+
<div id="whatsapp-qr" class="qr-area">
|
|
186
|
+
<p>Starting pairing...</p>
|
|
187
|
+
</div>
|
|
188
|
+
<p class="form-help" style="margin-top:12px">Once paired, this page will update automatically.</p>
|
|
189
|
+
<button class="btn btn-ghost" onclick="hideWhatsAppPanel()">Cancel</button>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<!-- CREDENTIALS TAB -->
|
|
194
|
+
<div id="panel-credentials" class="tab-panel">
|
|
195
|
+
<section class="section">
|
|
196
|
+
<h2 class="section-title">Credential Vault</h2>
|
|
197
|
+
<p class="section-desc">Stored logins for services the agent accesses via browser (Gmail, Drive, CRM, etc.)</p>
|
|
198
|
+
|
|
199
|
+
<div id="vault-list"></div>
|
|
200
|
+
|
|
201
|
+
<div class="form-panel" style="margin-top:16px">
|
|
202
|
+
<h3>Add credential</h3>
|
|
203
|
+
<label>Service name
|
|
204
|
+
<input type="text" id="vault-service" placeholder="e.g. gmail, salesforce, shipstation">
|
|
205
|
+
</label>
|
|
206
|
+
<label>Login URL
|
|
207
|
+
<input type="text" id="vault-url" placeholder="https://...">
|
|
208
|
+
</label>
|
|
209
|
+
<label>Username / email
|
|
210
|
+
<input type="text" id="vault-username">
|
|
211
|
+
</label>
|
|
212
|
+
<label>Password
|
|
213
|
+
<input type="password" id="vault-password">
|
|
214
|
+
</label>
|
|
215
|
+
<label>Notes for agent
|
|
216
|
+
<textarea id="vault-notes" placeholder="e.g. Use the shipping dashboard, not the admin panel"></textarea>
|
|
217
|
+
</label>
|
|
218
|
+
<div class="form-actions">
|
|
219
|
+
<button class="btn" onclick="addVaultEntry()">Add to vault</button>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</section>
|
|
223
|
+
</div>
|
|
224
|
+
</main>
|
|
225
|
+
|
|
226
|
+
<footer>
|
|
227
|
+
<p>Your files, credentials, and memory stay on this device.</p>
|
|
228
|
+
</footer>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<!-- TEAM DRAWER -->
|
|
232
|
+
<div id="drawer-backdrop" class="drawer-backdrop" onclick="closeDrawer()"></div>
|
|
233
|
+
<aside id="drawer" class="drawer">
|
|
234
|
+
<h3 id="drawer-title">Edit member</h3>
|
|
235
|
+
<div>
|
|
236
|
+
<label>WhatsApp number (E.164)
|
|
237
|
+
<input type="text" id="handle-whatsapp" placeholder="+13025551234">
|
|
238
|
+
</label>
|
|
239
|
+
</div>
|
|
240
|
+
<div>
|
|
241
|
+
<label>Telegram user ID (numeric)
|
|
242
|
+
<input type="text" id="handle-telegram" placeholder="6549960466" inputmode="numeric">
|
|
243
|
+
<small style="color:var(--text-faint);display:block;margin-top:4px">Message <a href="https://t.me/userinfobot" target="_blank" style="color:var(--accent)">@userinfobot</a> on Telegram to get your ID. Usernames aren't matched.</small>
|
|
244
|
+
</label>
|
|
245
|
+
</div>
|
|
246
|
+
<div>
|
|
247
|
+
<label>Teams Azure AD object id
|
|
248
|
+
<input type="text" id="handle-teams" placeholder="xxxxxxxx-xxxx-...">
|
|
249
|
+
</label>
|
|
250
|
+
</div>
|
|
251
|
+
<div>
|
|
252
|
+
<label style="margin-bottom:6px">Allowed channels</label>
|
|
253
|
+
<div id="drawer-channels" class="checklist"></div>
|
|
254
|
+
</div>
|
|
255
|
+
<div>
|
|
256
|
+
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:6px">
|
|
257
|
+
<label>Allowed workflows</label>
|
|
258
|
+
<a href="#" id="toggle-all-workflows" onclick="toggleAllWorkflows(event)" style="font-size:11px;color:var(--accent);text-decoration:none">Select all</a>
|
|
259
|
+
</div>
|
|
260
|
+
<div id="drawer-workflows" class="checklist"></div>
|
|
261
|
+
</div>
|
|
262
|
+
<div class="drawer-actions">
|
|
263
|
+
<button class="btn btn-ghost" onclick="closeDrawer()">Cancel</button>
|
|
264
|
+
<button class="btn btn-danger" onclick="removeCurrentMember()">Remove</button>
|
|
265
|
+
<button id="drawer-save-btn" class="btn" onclick="saveDrawer()">Save</button>
|
|
266
|
+
</div>
|
|
267
|
+
</aside>
|
|
268
|
+
|
|
269
|
+
<script>
|
|
270
|
+
// ---------- theme ----------
|
|
271
|
+
function initTheme() {
|
|
272
|
+
const saved = localStorage.getItem("leedab-theme") || "dark";
|
|
273
|
+
document.documentElement.setAttribute("data-theme", saved);
|
|
274
|
+
updateThemeIcon(saved);
|
|
275
|
+
}
|
|
276
|
+
function toggleTheme() {
|
|
277
|
+
const current = document.documentElement.getAttribute("data-theme") || "dark";
|
|
278
|
+
const next = current === "dark" ? "light" : "dark";
|
|
279
|
+
document.documentElement.setAttribute("data-theme", next);
|
|
280
|
+
localStorage.setItem("leedab-theme", next);
|
|
281
|
+
updateThemeIcon(next);
|
|
282
|
+
}
|
|
283
|
+
function updateThemeIcon(theme) {
|
|
284
|
+
const sun = document.getElementById("theme-icon-sun");
|
|
285
|
+
const moon = document.getElementById("theme-icon-moon");
|
|
286
|
+
if (sun && moon) {
|
|
287
|
+
sun.style.display = theme === "dark" ? "block" : "none";
|
|
288
|
+
moon.style.display = theme === "light" ? "block" : "none";
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
initTheme();
|
|
292
|
+
|
|
293
|
+
// ---------- tabs ----------
|
|
294
|
+
const TABS = ["team", "channels", "credentials"];
|
|
295
|
+
function switchTab(name) {
|
|
296
|
+
if (!TABS.includes(name)) name = "team";
|
|
297
|
+
for (const t of TABS) {
|
|
298
|
+
document.getElementById(`tab-${t}`).classList.toggle("active", t === name);
|
|
299
|
+
document.getElementById(`panel-${t}`).classList.toggle("active", t === name);
|
|
300
|
+
}
|
|
301
|
+
history.replaceState(null, "", `#${name}`);
|
|
302
|
+
// Lazy-load per-tab data
|
|
303
|
+
if (name === "channels") refreshStatus();
|
|
304
|
+
if (name === "credentials") loadVault();
|
|
305
|
+
}
|
|
306
|
+
function initialTab() {
|
|
307
|
+
const hash = (location.hash || "").replace(/^#/, "");
|
|
308
|
+
if (TABS.includes(hash)) return hash;
|
|
309
|
+
return "team";
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---------- state ----------
|
|
313
|
+
const CHANNELS = [
|
|
314
|
+
{ id: "whatsapp", label: "WhatsApp" },
|
|
315
|
+
{ id: "telegram", label: "Telegram" },
|
|
316
|
+
{ id: "teams", label: "Microsoft Teams" }
|
|
317
|
+
];
|
|
318
|
+
let WORKFLOWS = [];
|
|
319
|
+
let TEAM = [];
|
|
320
|
+
let LICENSE = null;
|
|
321
|
+
let editing = null;
|
|
322
|
+
|
|
323
|
+
document.addEventListener("DOMContentLoaded", async () => {
|
|
324
|
+
const initial = initialTab();
|
|
325
|
+
switchTab(initial);
|
|
326
|
+
await Promise.all([loadWorkflows(), loadLicense()]);
|
|
327
|
+
await loadTeam();
|
|
328
|
+
if (initial === "channels") refreshStatus();
|
|
329
|
+
if (initial === "credentials") loadVault();
|
|
330
|
+
});
|
|
331
|
+
window.addEventListener("hashchange", () => switchTab(initialTab()));
|
|
332
|
+
|
|
333
|
+
// ---------- license ----------
|
|
334
|
+
async function loadLicense() {
|
|
335
|
+
try {
|
|
336
|
+
const res = await fetch("/api/license");
|
|
337
|
+
LICENSE = await res.json();
|
|
338
|
+
} catch { LICENSE = null; }
|
|
339
|
+
renderSeatChip();
|
|
340
|
+
}
|
|
341
|
+
function renderSeatChip() {
|
|
342
|
+
const chip = document.getElementById("seat-chip");
|
|
343
|
+
if (!LICENSE || !LICENSE.maxSeats) { chip.style.display = "none"; return; }
|
|
344
|
+
const { seatsUsed, maxSeats, tier } = LICENSE;
|
|
345
|
+
const tierLabel = tier && tier !== "none" ? ` on ${tier.charAt(0).toUpperCase() + tier.slice(1)}` : "";
|
|
346
|
+
chip.textContent = `${seatsUsed} / ${maxSeats} seats${tierLabel}`;
|
|
347
|
+
if (seatsUsed >= maxSeats) chip.style.color = "var(--danger, #b91c1c)";
|
|
348
|
+
else chip.style.color = "var(--text-faint)";
|
|
349
|
+
updateInviteButtonState();
|
|
350
|
+
chip.style.display = "";
|
|
351
|
+
}
|
|
352
|
+
function updateInviteButtonState() {
|
|
353
|
+
const btn = document.getElementById("invite-btn");
|
|
354
|
+
if (!btn || !LICENSE) return;
|
|
355
|
+
if (LICENSE.maxSeats && LICENSE.seatsUsed >= LICENSE.maxSeats) {
|
|
356
|
+
btn.disabled = true;
|
|
357
|
+
btn.title = "Upgrade your plan to add more members";
|
|
358
|
+
} else {
|
|
359
|
+
btn.disabled = false;
|
|
360
|
+
btn.title = "";
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ---------- team ----------
|
|
365
|
+
async function loadWorkflows() {
|
|
366
|
+
try { const res = await fetch("/api/workflows"); WORKFLOWS = await res.json(); }
|
|
367
|
+
catch { WORKFLOWS = []; }
|
|
368
|
+
}
|
|
369
|
+
async function loadTeam() {
|
|
370
|
+
const container = document.getElementById("team-list");
|
|
371
|
+
container.innerHTML = '<div class="vault-empty" style="color:var(--text-faint)">Loading...</div>';
|
|
372
|
+
try {
|
|
373
|
+
const res = await fetch("/api/team");
|
|
374
|
+
const data = await res.json();
|
|
375
|
+
if (!res.ok) {
|
|
376
|
+
container.innerHTML = `<div class="vault-empty">${esc(data.error || "Could not load team.")}</div>`;
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
TEAM = Array.isArray(data) ? data : [];
|
|
380
|
+
if (!TEAM.length) {
|
|
381
|
+
container.innerHTML = '<div class="vault-empty">No team members yet. Add someone below.</div>';
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
container.innerHTML = `
|
|
385
|
+
<table class="team-table">
|
|
386
|
+
<thead>
|
|
387
|
+
<tr><th>Name</th><th>Email</th><th>Role</th><th>Channels</th><th>Workflows</th><th>Added</th></tr>
|
|
388
|
+
</thead>
|
|
389
|
+
<tbody>
|
|
390
|
+
${TEAM.map((m) => renderRow(m)).join("")}
|
|
391
|
+
</tbody>
|
|
392
|
+
</table>`;
|
|
393
|
+
container.querySelectorAll("tr.row").forEach((tr) => {
|
|
394
|
+
tr.addEventListener("click", () => openDrawer(tr.dataset.id));
|
|
395
|
+
});
|
|
396
|
+
} catch {
|
|
397
|
+
container.innerHTML = '<div class="vault-empty">Could not load team.</div>';
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function renderRow(m) {
|
|
401
|
+
const chanList = m.allowedChannels || [];
|
|
402
|
+
const channels = chanList.length
|
|
403
|
+
? `<span class="chip">${esc(channelLabel(chanList[0]))}</span>`
|
|
404
|
+
+ (chanList.length > 1 ? `<span class="chip more">+${chanList.length - 1}</span>` : "")
|
|
405
|
+
: '<span class="chip empty">none</span>';
|
|
406
|
+
const wfList = m.allowedWorkflows || [];
|
|
407
|
+
const wfs = wfList.length
|
|
408
|
+
? `<span class="chip">${esc(workflowTitle(wfList[0]))}</span>`
|
|
409
|
+
+ (wfList.length > 1 ? `<span class="chip more">+${wfList.length - 1}</span>` : "")
|
|
410
|
+
: (m.role === "admin" || m.role === "owner" ? '<span class="chip">all</span>' : '<span class="chip empty">none</span>');
|
|
411
|
+
return `
|
|
412
|
+
<tr class="row" data-id="${esc(m.id)}">
|
|
413
|
+
<td>${esc(m.name || "-")}</td>
|
|
414
|
+
<td>${m.email ? esc(m.email) : '<span style="color:var(--text-faint)">-</span>'}</td>
|
|
415
|
+
<td><span class="role-badge role-${esc(m.role)}">${esc(m.role)}</span></td>
|
|
416
|
+
<td><div class="chip-group">${channels}</div></td>
|
|
417
|
+
<td><div class="chip-group">${wfs}</div></td>
|
|
418
|
+
<td style="color:var(--text-dim)">${m.joined_at ? new Date(m.joined_at).toLocaleDateString() : "-"}</td>
|
|
419
|
+
</tr>`;
|
|
420
|
+
}
|
|
421
|
+
function channelLabel(id) { const f = CHANNELS.find((c) => c.id === id); return f ? f.label : id; }
|
|
422
|
+
function workflowTitle(id) { const f = WORKFLOWS.find((w) => w.id === id); return f ? f.title : id; }
|
|
423
|
+
|
|
424
|
+
async function addTeamMember() {
|
|
425
|
+
const name = document.getElementById("member-name").value.trim();
|
|
426
|
+
const email = document.getElementById("member-email").value.trim();
|
|
427
|
+
const role = document.getElementById("member-role").value;
|
|
428
|
+
if (!name) { showToast("Name is required", "error"); return; }
|
|
429
|
+
const btn = document.getElementById("invite-btn");
|
|
430
|
+
const originalText = btn.textContent;
|
|
431
|
+
btn.disabled = true;
|
|
432
|
+
btn.textContent = "Adding...";
|
|
433
|
+
try {
|
|
434
|
+
const res = await fetch("/api/team", {
|
|
435
|
+
method: "POST",
|
|
436
|
+
headers: { "Content-Type": "application/json" },
|
|
437
|
+
body: JSON.stringify({ name, email: email || undefined, role }),
|
|
438
|
+
});
|
|
439
|
+
const data = await res.json();
|
|
440
|
+
if (!res.ok) { showToast(data.error || "Failed to add member", "error"); return; }
|
|
441
|
+
document.getElementById("member-name").value = "";
|
|
442
|
+
document.getElementById("member-email").value = "";
|
|
443
|
+
showToast(`Added ${name}`, "success");
|
|
444
|
+
await loadLicense();
|
|
445
|
+
await loadTeam();
|
|
446
|
+
} catch { showToast("Failed to add member", "error"); }
|
|
447
|
+
finally {
|
|
448
|
+
btn.textContent = originalText;
|
|
449
|
+
updateInviteButtonState();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function removeCurrentMember() {
|
|
454
|
+
if (!editing) return;
|
|
455
|
+
if (!confirm(`Remove ${editing.name}? They will lose access to the agent.`)) return;
|
|
456
|
+
try {
|
|
457
|
+
await fetch(`/api/team?id=${encodeURIComponent(editing.id)}`, { method: "DELETE" });
|
|
458
|
+
closeDrawer();
|
|
459
|
+
showToast("Member removed", "success");
|
|
460
|
+
await loadLicense();
|
|
461
|
+
await loadTeam();
|
|
462
|
+
} catch { showToast("Failed to remove member", "error"); }
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function openDrawer(id) {
|
|
466
|
+
const member = TEAM.find((m) => m.id === id);
|
|
467
|
+
if (!member) return;
|
|
468
|
+
editing = member;
|
|
469
|
+
document.getElementById("drawer-title").textContent = `Edit ${member.name || member.email || "member"}`;
|
|
470
|
+
document.getElementById("handle-whatsapp").value = member.handles?.whatsapp || "";
|
|
471
|
+
document.getElementById("handle-telegram").value = member.handles?.telegram || "";
|
|
472
|
+
document.getElementById("handle-teams").value = member.handles?.teams || "";
|
|
473
|
+
|
|
474
|
+
document.getElementById("drawer-channels").innerHTML = CHANNELS.map((c) => {
|
|
475
|
+
const checked = (member.allowedChannels || []).includes(c.id) ? "checked" : "";
|
|
476
|
+
return `<label><input type="checkbox" value="${esc(c.id)}" ${checked}> <span>${esc(c.label)}</span></label>`;
|
|
477
|
+
}).join("");
|
|
478
|
+
|
|
479
|
+
document.getElementById("drawer-workflows").innerHTML = WORKFLOWS.map((w) => {
|
|
480
|
+
const checked = (member.allowedWorkflows || []).includes(w.id) ? "checked" : "";
|
|
481
|
+
return `
|
|
482
|
+
<label>
|
|
483
|
+
<input type="checkbox" value="${esc(w.id)}" ${checked}>
|
|
484
|
+
<span>${esc(w.title)}<small>${esc(w.description)}</small></span>
|
|
485
|
+
</label>`;
|
|
486
|
+
}).join("");
|
|
487
|
+
const allWfChecked = WORKFLOWS.length && (member.allowedWorkflows || []).length >= WORKFLOWS.length;
|
|
488
|
+
document.getElementById("toggle-all-workflows").textContent = allWfChecked ? "Deselect all" : "Select all";
|
|
489
|
+
|
|
490
|
+
const removeBtn = document.querySelector(".drawer-actions .btn-danger");
|
|
491
|
+
if (removeBtn) removeBtn.style.display = member.role === "owner" ? "none" : "";
|
|
492
|
+
|
|
493
|
+
document.getElementById("drawer").classList.add("open");
|
|
494
|
+
document.getElementById("drawer-backdrop").classList.add("open");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function closeDrawer() {
|
|
498
|
+
document.getElementById("drawer").classList.remove("open");
|
|
499
|
+
document.getElementById("drawer-backdrop").classList.remove("open");
|
|
500
|
+
editing = null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function toggleAllWorkflows(e) {
|
|
504
|
+
e.preventDefault();
|
|
505
|
+
const boxes = document.querySelectorAll("#drawer-workflows input[type=checkbox]");
|
|
506
|
+
const allChecked = [...boxes].every((b) => b.checked);
|
|
507
|
+
boxes.forEach((b) => b.checked = !allChecked);
|
|
508
|
+
document.getElementById("toggle-all-workflows").textContent = allChecked ? "Select all" : "Deselect all";
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function saveDrawer() {
|
|
512
|
+
if (!editing) return;
|
|
513
|
+
const btn = document.getElementById("drawer-save-btn");
|
|
514
|
+
btn.disabled = true;
|
|
515
|
+
btn.textContent = "Saving...";
|
|
516
|
+
const handles = {
|
|
517
|
+
whatsapp: document.getElementById("handle-whatsapp").value.trim(),
|
|
518
|
+
telegram: document.getElementById("handle-telegram").value.trim(),
|
|
519
|
+
teams: document.getElementById("handle-teams").value.trim(),
|
|
520
|
+
};
|
|
521
|
+
const allowedChannels = [...document.querySelectorAll("#drawer-channels input[type=checkbox]:checked")].map((el) => el.value);
|
|
522
|
+
const allowedWorkflows = [...document.querySelectorAll("#drawer-workflows input[type=checkbox]:checked")].map((el) => el.value);
|
|
523
|
+
try {
|
|
524
|
+
const res = await fetch("/api/team/permissions", {
|
|
525
|
+
method: "PUT",
|
|
526
|
+
headers: { "Content-Type": "application/json" },
|
|
527
|
+
body: JSON.stringify({ memberId: editing.id, handles, allowedWorkflows, allowedChannels }),
|
|
528
|
+
});
|
|
529
|
+
const data = await res.json();
|
|
530
|
+
if (!res.ok) { showToast(data.error || "Failed to save", "error"); return; }
|
|
531
|
+
showToast("Saved", "success");
|
|
532
|
+
closeDrawer();
|
|
533
|
+
await loadTeam();
|
|
534
|
+
} catch { showToast("Failed to save", "error"); }
|
|
535
|
+
finally { btn.disabled = false; btn.textContent = "Save"; }
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function showToast(message, type) {
|
|
539
|
+
const toast = document.getElementById("toast");
|
|
540
|
+
toast.innerHTML = "";
|
|
541
|
+
toast.textContent = message;
|
|
542
|
+
toast.className = `toast ${type}`;
|
|
543
|
+
setTimeout(() => toast.classList.add("hidden"), 4000);
|
|
544
|
+
}
|
|
545
|
+
function esc(str) {
|
|
546
|
+
return String(str ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ---------- channels ----------
|
|
550
|
+
async function refreshStatus() {
|
|
551
|
+
try {
|
|
552
|
+
const res = await fetch("/api/status");
|
|
553
|
+
const status = await res.json();
|
|
554
|
+
for (const [key, info] of Object.entries(status)) {
|
|
555
|
+
const card = document.getElementById(`card-${key}`);
|
|
556
|
+
const statusEl = document.getElementById(`status-${key}`);
|
|
557
|
+
const btn = card?.querySelector(".btn");
|
|
558
|
+
if (info.connected) {
|
|
559
|
+
card?.classList.add("connected");
|
|
560
|
+
if (statusEl) statusEl.innerHTML = '<span class="dot"></span> Connected';
|
|
561
|
+
if (btn) { btn.textContent = "Connected"; btn.className = "btn btn-connected"; btn.disabled = true; btn.onclick = null; }
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
} catch {}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function connectWhatsApp() {
|
|
568
|
+
const panel = document.getElementById("whatsapp-panel");
|
|
569
|
+
panel.classList.remove("hidden");
|
|
570
|
+
const qr = document.getElementById("whatsapp-qr");
|
|
571
|
+
qr.innerHTML = `<p>Generating QR code...</p>`;
|
|
572
|
+
fetch("/api/whatsapp/connect", { method: "POST" })
|
|
573
|
+
.then((r) => r.json())
|
|
574
|
+
.then((data) => {
|
|
575
|
+
if (data.error) { qr.innerHTML = `<p style="color:#ef4444">${esc(data.error)}</p>`; return; }
|
|
576
|
+
if (data.qr) {
|
|
577
|
+
const lines = data.qr.split("\n").filter((l) => l.includes("\u2588") || l.includes("\u2584") || l.includes("\u2580"));
|
|
578
|
+
const qrText = lines.join("\n");
|
|
579
|
+
qr.innerHTML = `<pre style="font-family:monospace;font-size:4px;line-height:4px;letter-spacing:0;background:white;color:black;padding:16px;display:inline-block;border-radius:8px;transform:scale(2);transform-origin:center;margin:40px 0">${esc(qrText)}</pre>`;
|
|
580
|
+
pollWhatsAppStatus();
|
|
581
|
+
} else { qr.innerHTML = `<p>Waiting for QR code... try again in a moment.</p>`; }
|
|
582
|
+
})
|
|
583
|
+
.catch(() => { qr.innerHTML = `<p style="color:#ef4444">Connection failed. Try again.</p>`; });
|
|
584
|
+
}
|
|
585
|
+
function pollWhatsAppStatus() {
|
|
586
|
+
const interval = setInterval(async () => {
|
|
587
|
+
const res = await fetch("/api/status");
|
|
588
|
+
const status = await res.json();
|
|
589
|
+
if (status.whatsapp.connected) {
|
|
590
|
+
clearInterval(interval);
|
|
591
|
+
hideWhatsAppPanel();
|
|
592
|
+
showToast("WhatsApp connected!", "success");
|
|
593
|
+
refreshStatus();
|
|
594
|
+
}
|
|
595
|
+
}, 3000);
|
|
596
|
+
setTimeout(() => clearInterval(interval), 120000);
|
|
597
|
+
}
|
|
598
|
+
function hideWhatsAppPanel() { document.getElementById("whatsapp-panel").classList.add("hidden"); }
|
|
599
|
+
|
|
600
|
+
function showTelegramForm() { document.getElementById("telegram-form").classList.remove("hidden"); }
|
|
601
|
+
function hideTelegramForm() { document.getElementById("telegram-form").classList.add("hidden"); }
|
|
602
|
+
async function connectTelegram() {
|
|
603
|
+
const token = document.getElementById("telegram-token").value.trim();
|
|
604
|
+
if (!token) { showToast("Bot token is required", "error"); return; }
|
|
605
|
+
try {
|
|
606
|
+
const res = await fetch("/api/telegram/connect", {
|
|
607
|
+
method: "POST",
|
|
608
|
+
headers: { "Content-Type": "application/json" },
|
|
609
|
+
body: JSON.stringify({ token }),
|
|
610
|
+
});
|
|
611
|
+
const data = await res.json();
|
|
612
|
+
if (data.error) showToast(data.error, "error");
|
|
613
|
+
else { hideTelegramForm(); showToast("Telegram connected!", "success"); refreshStatus(); }
|
|
614
|
+
} catch { showToast("Connection failed", "error"); }
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function showTeamsForm() { document.getElementById("teams-form").classList.remove("hidden"); }
|
|
618
|
+
function hideTeamsForm() { document.getElementById("teams-form").classList.add("hidden"); }
|
|
619
|
+
async function connectTeams() {
|
|
620
|
+
const appId = document.getElementById("teams-app-id").value.trim();
|
|
621
|
+
const tenantId = document.getElementById("teams-tenant-id").value.trim();
|
|
622
|
+
if (!appId || !tenantId) { showToast("All fields are required", "error"); return; }
|
|
623
|
+
try {
|
|
624
|
+
const res = await fetch("/api/teams/connect", {
|
|
625
|
+
method: "POST",
|
|
626
|
+
headers: { "Content-Type": "application/json" },
|
|
627
|
+
body: JSON.stringify({ appId, tenantId }),
|
|
628
|
+
});
|
|
629
|
+
const data = await res.json();
|
|
630
|
+
if (data.error) showToast(data.error, "error");
|
|
631
|
+
else { hideTeamsForm(); showToast("Teams credentials saved!", "success"); refreshStatus(); }
|
|
632
|
+
} catch { showToast("Connection failed", "error"); }
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ---------- credentials ----------
|
|
636
|
+
async function loadVault() {
|
|
637
|
+
const container = document.getElementById("vault-list");
|
|
638
|
+
if (!container) return;
|
|
639
|
+
try {
|
|
640
|
+
const res = await fetch("/api/vault");
|
|
641
|
+
const entries = await res.json();
|
|
642
|
+
if (!entries.length) { container.innerHTML = '<div class="vault-empty">No credentials stored yet.</div>'; return; }
|
|
643
|
+
container.innerHTML = `
|
|
644
|
+
<table class="vault-table">
|
|
645
|
+
<thead><tr><th>Service</th><th>URL</th><th>Username</th><th></th></tr></thead>
|
|
646
|
+
<tbody>
|
|
647
|
+
${entries.map((e) => `
|
|
648
|
+
<tr>
|
|
649
|
+
<td>${esc(e.service)}</td>
|
|
650
|
+
<td>${e.url ? esc(e.url) : '<span style="color:var(--text-faint)">—</span>'}</td>
|
|
651
|
+
<td>${e.username ? esc(e.username) : '<span style="color:var(--text-faint)">—</span>'}</td>
|
|
652
|
+
<td><button class="btn btn-danger" onclick="removeVaultEntry('${esc(e.service)}')">Remove</button></td>
|
|
653
|
+
</tr>
|
|
654
|
+
`).join("")}
|
|
655
|
+
</tbody>
|
|
656
|
+
</table>`;
|
|
657
|
+
} catch { container.innerHTML = '<div class="vault-empty">Could not load vault.</div>'; }
|
|
658
|
+
}
|
|
659
|
+
async function addVaultEntry() {
|
|
660
|
+
const service = document.getElementById("vault-service").value.trim();
|
|
661
|
+
const url = document.getElementById("vault-url").value.trim();
|
|
662
|
+
const username = document.getElementById("vault-username").value.trim();
|
|
663
|
+
const password = document.getElementById("vault-password").value;
|
|
664
|
+
const notes = document.getElementById("vault-notes").value.trim();
|
|
665
|
+
if (!service) { showToast("Service name is required", "error"); return; }
|
|
666
|
+
try {
|
|
667
|
+
await fetch("/api/vault", {
|
|
668
|
+
method: "POST",
|
|
669
|
+
headers: { "Content-Type": "application/json" },
|
|
670
|
+
body: JSON.stringify({ service, url, username, password, notes }),
|
|
671
|
+
});
|
|
672
|
+
for (const id of ["vault-service","vault-url","vault-username","vault-password","vault-notes"]) {
|
|
673
|
+
document.getElementById(id).value = "";
|
|
674
|
+
}
|
|
675
|
+
showToast(`Added ${service} to vault`, "success");
|
|
676
|
+
loadVault();
|
|
677
|
+
} catch { showToast("Failed to add credential", "error"); }
|
|
678
|
+
}
|
|
679
|
+
async function removeVaultEntry(service) {
|
|
680
|
+
try {
|
|
681
|
+
await fetch(`/api/vault?service=${encodeURIComponent(service)}`, { method: "DELETE" });
|
|
682
|
+
showToast(`Removed ${service}`, "success");
|
|
683
|
+
loadVault();
|
|
684
|
+
} catch { showToast("Failed to remove credential", "error"); }
|
|
685
|
+
}
|
|
686
|
+
</script>
|
|
687
|
+
</body>
|
|
688
|
+
</html>
|