a2acalling 0.1.7 → 0.1.9

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.
@@ -0,0 +1,341 @@
1
+ const state = {
2
+ settings: null,
3
+ contacts: [],
4
+ calls: [],
5
+ invites: []
6
+ };
7
+
8
+ function showNotice(message) {
9
+ const el = document.getElementById('notice');
10
+ el.textContent = message;
11
+ el.style.display = 'block';
12
+ setTimeout(() => {
13
+ el.style.display = 'none';
14
+ }, 3500);
15
+ }
16
+
17
+ async function request(path, options = {}) {
18
+ const res = await fetch(`/api/a2a/dashboard${path}`, {
19
+ headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
20
+ ...options
21
+ });
22
+ const payload = await res.json().catch(() => ({}));
23
+ if (!res.ok || payload.success === false) {
24
+ throw new Error(payload.message || payload.error || `Request failed: ${res.status}`);
25
+ }
26
+ return payload;
27
+ }
28
+
29
+ function toLines(values) {
30
+ return (values || []).join('\n');
31
+ }
32
+
33
+ function fromLines(value) {
34
+ return value
35
+ .split('\n')
36
+ .map(v => v.trim())
37
+ .filter(Boolean);
38
+ }
39
+
40
+ function fmtDate(value) {
41
+ if (!value) return '-';
42
+ try {
43
+ return new Date(value).toLocaleString();
44
+ } catch (err) {
45
+ return String(value);
46
+ }
47
+ }
48
+
49
+ function bindTabs() {
50
+ document.querySelectorAll('.tab').forEach(btn => {
51
+ btn.addEventListener('click', () => {
52
+ const tab = btn.dataset.tab;
53
+ document.querySelectorAll('.tab').forEach(b => b.classList.remove('is-active'));
54
+ document.querySelectorAll('.panel').forEach(p => p.classList.remove('is-active'));
55
+ btn.classList.add('is-active');
56
+ document.getElementById(`tab-${tab}`).classList.add('is-active');
57
+ });
58
+ });
59
+ }
60
+
61
+ function renderContacts() {
62
+ const tbody = document.querySelector('#contacts-table tbody');
63
+ tbody.innerHTML = '';
64
+ state.contacts.forEach(contact => {
65
+ const tr = document.createElement('tr');
66
+ tr.innerHTML = `
67
+ <td>${contact.name || '-'}</td>
68
+ <td>${contact.owner || '-'}</td>
69
+ <td>${contact.status || '-'}</td>
70
+ <td>${contact.call_count || 0}</td>
71
+ <td>${(contact.last_summary || contact.last_owner_summary || '-').slice(0, 120)}</td>
72
+ `;
73
+ tr.addEventListener('click', () => loadCallsForContact(contact.id, contact.name));
74
+ tbody.appendChild(tr);
75
+ });
76
+ }
77
+
78
+ async function loadContacts() {
79
+ const payload = await request('/contacts');
80
+ state.contacts = payload.contacts || [];
81
+ renderContacts();
82
+ }
83
+
84
+ function renderCalls() {
85
+ const tbody = document.querySelector('#calls-table tbody');
86
+ tbody.innerHTML = '';
87
+ state.calls.forEach(call => {
88
+ const tr = document.createElement('tr');
89
+ tr.innerHTML = `
90
+ <td>${call.id}</td>
91
+ <td>${call.contact?.name || call.contact_name || '-'}</td>
92
+ <td>${call.status || '-'}</td>
93
+ <td>${call.message_count || 0}</td>
94
+ <td>${fmtDate(call.last_message_at)}</td>
95
+ <td>${(call.summary || call.owner_summary || '-').slice(0, 120)}</td>
96
+ `;
97
+ tr.addEventListener('click', () => loadCallDetail(call.id));
98
+ tbody.appendChild(tr);
99
+ });
100
+ }
101
+
102
+ async function loadCalls() {
103
+ const payload = await request('/calls?limit=200');
104
+ state.calls = payload.calls || [];
105
+ renderCalls();
106
+ }
107
+
108
+ async function loadCallDetail(conversationId) {
109
+ const payload = await request(`/calls/${encodeURIComponent(conversationId)}?messages=40`);
110
+ const call = payload.call;
111
+ const el = document.getElementById('call-detail');
112
+ const messages = (call.recentMessages || [])
113
+ .map(msg => `[${fmtDate(msg.timestamp)}] ${msg.direction}: ${msg.content}`)
114
+ .join('\n\n');
115
+ el.innerHTML = `
116
+ <h3>Call Detail: ${call.id}</h3>
117
+ <p><strong>Contact:</strong> ${call.contact?.name || call.contact || '-'}</p>
118
+ <p><strong>Status:</strong> ${call.status || '-'}</p>
119
+ <p><strong>Summary:</strong> ${(call.summary || call.ownerContext?.summary || '-')}</p>
120
+ <pre class="summary">${messages || 'No messages recorded.'}</pre>
121
+ `;
122
+ }
123
+
124
+ async function loadCallsForContact(contactId, contactName) {
125
+ const payload = await request(`/contacts/${encodeURIComponent(contactId)}/calls?limit=100`);
126
+ const calls = payload.calls || [];
127
+ const el = document.getElementById('contact-calls');
128
+ const rows = calls.map(call => {
129
+ return `
130
+ <tr>
131
+ <td>${call.id}</td>
132
+ <td>${call.status || '-'}</td>
133
+ <td>${fmtDate(call.last_message_at)}</td>
134
+ <td>${(call.summary || call.owner_summary || '-').slice(0, 140)}</td>
135
+ </tr>
136
+ `;
137
+ }).join('');
138
+ el.innerHTML = `
139
+ <h3>Calls with ${contactName}</h3>
140
+ <table>
141
+ <thead><tr><th>ID</th><th>Status</th><th>Updated</th><th>Summary</th></tr></thead>
142
+ <tbody>${rows || '<tr><td colspan="4">No calls found.</td></tr>'}</tbody>
143
+ </table>
144
+ `;
145
+ }
146
+
147
+ function fillTierSelects() {
148
+ const tiers = (state.settings?.tiers || []).slice().sort((a, b) => a.id.localeCompare(b.id));
149
+ const tierSelect = document.getElementById('tier-select');
150
+ const copyFrom = document.getElementById('copy-from-tier');
151
+ const newTierCopy = document.getElementById('new-tier-copy-from');
152
+ const inviteTier = document.getElementById('invite-tier');
153
+
154
+ [tierSelect, copyFrom, inviteTier].forEach(el => { el.innerHTML = ''; });
155
+ newTierCopy.innerHTML = '<option value="">None</option>';
156
+
157
+ tiers.forEach(tier => {
158
+ const option = new Option(`${tier.id} (${tier.name || tier.id})`, tier.id);
159
+ tierSelect.add(option.cloneNode(true));
160
+ copyFrom.add(option.cloneNode(true));
161
+ inviteTier.add(option.cloneNode(true));
162
+ newTierCopy.add(option.cloneNode(true));
163
+ });
164
+
165
+ if (tiers.length > 0) {
166
+ tierSelect.value = tiers[0].id;
167
+ copyFrom.value = tiers[0].id;
168
+ inviteTier.value = tiers[0].id;
169
+ renderTierEditor(tiers[0].id);
170
+ }
171
+ }
172
+
173
+ function renderTierEditor(tierId) {
174
+ const tier = (state.settings?.tiers || []).find(t => t.id === tierId);
175
+ if (!tier) return;
176
+
177
+ document.getElementById('tier-id').value = tier.id;
178
+ document.getElementById('tier-name').value = tier.name || tier.id;
179
+ document.getElementById('tier-description').value = tier.description || '';
180
+ document.getElementById('tier-disclosure').value = tier.disclosure || 'minimal';
181
+ document.getElementById('tier-topics').value = toLines(tier.topics || []);
182
+ document.getElementById('tier-goals').value = toLines(tier.goals || []);
183
+ }
184
+
185
+ function bindSettingsActions() {
186
+ document.getElementById('tier-select').addEventListener('change', (e) => {
187
+ renderTierEditor(e.target.value);
188
+ });
189
+
190
+ document.getElementById('tier-form').addEventListener('submit', async (e) => {
191
+ e.preventDefault();
192
+ const tierId = document.getElementById('tier-id').value;
193
+ const body = {
194
+ name: document.getElementById('tier-name').value,
195
+ description: document.getElementById('tier-description').value,
196
+ disclosure: document.getElementById('tier-disclosure').value,
197
+ topics: fromLines(document.getElementById('tier-topics').value),
198
+ goals: fromLines(document.getElementById('tier-goals').value)
199
+ };
200
+ await request(`/settings/tiers/${encodeURIComponent(tierId)}`, {
201
+ method: 'PUT',
202
+ body: JSON.stringify(body)
203
+ });
204
+ showNotice(`Saved tier "${tierId}"`);
205
+ await loadSettings();
206
+ });
207
+
208
+ document.getElementById('copy-tier-btn').addEventListener('click', async () => {
209
+ const toTier = document.getElementById('tier-id').value;
210
+ const fromTier = document.getElementById('copy-from-tier').value;
211
+ if (!toTier || !fromTier || toTier === fromTier) return;
212
+ await request(`/settings/tiers/${encodeURIComponent(toTier)}/copy-from/${encodeURIComponent(fromTier)}`, {
213
+ method: 'POST'
214
+ });
215
+ showNotice(`Copied "${fromTier}" -> "${toTier}"`);
216
+ await loadSettings();
217
+ renderTierEditor(toTier);
218
+ });
219
+
220
+ document.getElementById('defaults-form').addEventListener('submit', async (e) => {
221
+ e.preventDefault();
222
+ await request('/settings/defaults', {
223
+ method: 'PUT',
224
+ body: JSON.stringify({
225
+ expiration: document.getElementById('defaults-expiration').value,
226
+ maxCalls: Number.parseInt(document.getElementById('defaults-max-calls').value, 10) || 100
227
+ })
228
+ });
229
+ showNotice('Saved defaults');
230
+ await loadSettings();
231
+ });
232
+
233
+ document.getElementById('new-tier-btn').addEventListener('click', () => {
234
+ document.getElementById('new-tier-id').focus();
235
+ });
236
+
237
+ document.getElementById('new-tier-form').addEventListener('submit', async (e) => {
238
+ e.preventDefault();
239
+ const tierId = document.getElementById('new-tier-id').value.trim();
240
+ const name = document.getElementById('new-tier-name').value.trim();
241
+ const copyFrom = document.getElementById('new-tier-copy-from').value;
242
+ if (!tierId) return;
243
+ await request('/settings/tiers', {
244
+ method: 'POST',
245
+ body: JSON.stringify({
246
+ id: tierId,
247
+ name: name || tierId,
248
+ copy_from: copyFrom || undefined
249
+ })
250
+ });
251
+ showNotice(`Created tier "${tierId}"`);
252
+ document.getElementById('new-tier-form').reset();
253
+ await loadSettings();
254
+ document.getElementById('tier-select').value = tierId;
255
+ renderTierEditor(tierId);
256
+ });
257
+ }
258
+
259
+ async function loadSettings() {
260
+ const payload = await request('/settings');
261
+ state.settings = payload;
262
+ fillTierSelects();
263
+ document.getElementById('defaults-expiration').value = payload.defaults?.expiration || '7d';
264
+ document.getElementById('defaults-max-calls').value = payload.defaults?.maxCalls || 100;
265
+ }
266
+
267
+ function renderInvites() {
268
+ const tbody = document.querySelector('#invites-table tbody');
269
+ tbody.innerHTML = '';
270
+ state.invites.forEach(invite => {
271
+ const tr = document.createElement('tr');
272
+ tr.innerHTML = `
273
+ <td>${invite.id}</td>
274
+ <td>${invite.name || '-'}</td>
275
+ <td>${invite.tier_label || invite.tier || '-'}</td>
276
+ <td>${invite.calls_made || 0}${invite.max_calls ? `/${invite.max_calls}` : ''}</td>
277
+ <td>${fmtDate(invite.expires_at)}</td>
278
+ <td>${invite.revoked ? 'revoked' : 'active'}</td>
279
+ <td><button data-revoke="${invite.id}" ${invite.revoked ? 'disabled' : ''}>Revoke</button></td>
280
+ `;
281
+ tbody.appendChild(tr);
282
+ });
283
+
284
+ tbody.querySelectorAll('button[data-revoke]').forEach(btn => {
285
+ btn.addEventListener('click', async () => {
286
+ const tokenId = btn.dataset.revoke;
287
+ await request(`/invites/${encodeURIComponent(tokenId)}/revoke`, { method: 'POST' });
288
+ showNotice(`Revoked ${tokenId}`);
289
+ await loadInvites();
290
+ });
291
+ });
292
+ }
293
+
294
+ async function loadInvites() {
295
+ const payload = await request('/invites?include_revoked=true');
296
+ state.invites = payload.invites || [];
297
+ renderInvites();
298
+ }
299
+
300
+ function bindInviteActions() {
301
+ document.getElementById('invite-form').addEventListener('submit', async (e) => {
302
+ e.preventDefault();
303
+ const body = {
304
+ name: document.getElementById('invite-name').value,
305
+ owner: document.getElementById('invite-owner').value,
306
+ tier: document.getElementById('invite-tier').value,
307
+ expires: document.getElementById('invite-expires').value,
308
+ max_calls: Number.parseInt(document.getElementById('invite-max-calls').value, 10),
309
+ notify: document.getElementById('invite-notify').value
310
+ };
311
+ const result = await request('/invites', {
312
+ method: 'POST',
313
+ body: JSON.stringify(body)
314
+ });
315
+ document.getElementById('invite-message').value = result.invite_message || result.invite_url;
316
+ showNotice('Invite created');
317
+ await loadInvites();
318
+ });
319
+ }
320
+
321
+ function bindRefreshButtons() {
322
+ document.getElementById('refresh-contacts').addEventListener('click', () => loadContacts().catch(err => showNotice(err.message)));
323
+ document.getElementById('refresh-calls').addEventListener('click', () => loadCalls().catch(err => showNotice(err.message)));
324
+ document.getElementById('refresh-invites').addEventListener('click', () => loadInvites().catch(err => showNotice(err.message)));
325
+ }
326
+
327
+ async function bootstrap() {
328
+ bindTabs();
329
+ bindSettingsActions();
330
+ bindInviteActions();
331
+ bindRefreshButtons();
332
+
333
+ try {
334
+ await Promise.all([loadSettings(), loadContacts(), loadCalls(), loadInvites()]);
335
+ showNotice('Dashboard loaded');
336
+ } catch (err) {
337
+ showNotice(err.message);
338
+ }
339
+ }
340
+
341
+ bootstrap();
@@ -0,0 +1,155 @@
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">
6
+ <title>A2A Dashboard</title>
7
+ <link rel="stylesheet" href="./style.css">
8
+ </head>
9
+ <body>
10
+ <header>
11
+ <h1>A2A Dashboard</h1>
12
+ <p>Contacts, calls, settings, and invites.</p>
13
+ </header>
14
+
15
+ <nav>
16
+ <button class="tab is-active" data-tab="contacts">Contacts</button>
17
+ <button class="tab" data-tab="calls">Calls</button>
18
+ <button class="tab" data-tab="settings">Settings</button>
19
+ <button class="tab" data-tab="invites">Invites</button>
20
+ </nav>
21
+
22
+ <main>
23
+ <section id="tab-contacts" class="panel is-active">
24
+ <div class="row">
25
+ <h2>Contacts</h2>
26
+ <button id="refresh-contacts">Refresh</button>
27
+ </div>
28
+ <table id="contacts-table">
29
+ <thead>
30
+ <tr>
31
+ <th>Name</th>
32
+ <th>Owner</th>
33
+ <th>Status</th>
34
+ <th>Calls</th>
35
+ <th>Last Summary</th>
36
+ </tr>
37
+ </thead>
38
+ <tbody></tbody>
39
+ </table>
40
+ <div id="contact-calls"></div>
41
+ </section>
42
+
43
+ <section id="tab-calls" class="panel">
44
+ <div class="row">
45
+ <h2>Calls</h2>
46
+ <button id="refresh-calls">Refresh</button>
47
+ </div>
48
+ <table id="calls-table">
49
+ <thead>
50
+ <tr>
51
+ <th>Conversation</th>
52
+ <th>Contact</th>
53
+ <th>Status</th>
54
+ <th>Messages</th>
55
+ <th>Updated</th>
56
+ <th>Summary</th>
57
+ </tr>
58
+ </thead>
59
+ <tbody></tbody>
60
+ </table>
61
+ <div id="call-detail"></div>
62
+ </section>
63
+
64
+ <section id="tab-settings" class="panel">
65
+ <h2>Tier Settings</h2>
66
+ <div class="row">
67
+ <label for="tier-select">Tier</label>
68
+ <select id="tier-select"></select>
69
+ <button id="new-tier-btn">New Tier</button>
70
+ </div>
71
+
72
+ <form id="tier-form">
73
+ <label>Tier ID <input id="tier-id" type="text" readonly></label>
74
+ <label>Name <input id="tier-name" type="text"></label>
75
+ <label>Description <input id="tier-description" type="text"></label>
76
+ <label>Disclosure <input id="tier-disclosure" type="text" placeholder="minimal"></label>
77
+ <label>Topics (one per line)<textarea id="tier-topics" rows="6"></textarea></label>
78
+ <label>Goals (one per line)<textarea id="tier-goals" rows="6"></textarea></label>
79
+ <div class="row">
80
+ <button type="submit">Save Tier</button>
81
+ </div>
82
+ </form>
83
+
84
+ <div class="row">
85
+ <label for="copy-from-tier">Copy from</label>
86
+ <select id="copy-from-tier"></select>
87
+ <button id="copy-tier-btn">Copy Tier</button>
88
+ </div>
89
+
90
+ <h3>Defaults</h3>
91
+ <form id="defaults-form">
92
+ <label>Expiration <input id="defaults-expiration" type="text" placeholder="7d"></label>
93
+ <label>Max Calls <input id="defaults-max-calls" type="number" min="1"></label>
94
+ <div class="row">
95
+ <button type="submit">Save Defaults</button>
96
+ </div>
97
+ </form>
98
+
99
+ <h3>New Tier</h3>
100
+ <form id="new-tier-form">
101
+ <label>Tier ID <input id="new-tier-id" type="text" placeholder="partners"></label>
102
+ <label>Name <input id="new-tier-name" type="text" placeholder="Partners"></label>
103
+ <label>Copy from
104
+ <select id="new-tier-copy-from">
105
+ <option value="">None</option>
106
+ </select>
107
+ </label>
108
+ <div class="row">
109
+ <button type="submit">Create Tier</button>
110
+ </div>
111
+ </form>
112
+ </section>
113
+
114
+ <section id="tab-invites" class="panel">
115
+ <h2>Generate Invite</h2>
116
+ <form id="invite-form">
117
+ <label>Name <input id="invite-name" type="text" required></label>
118
+ <label>Owner <input id="invite-owner" type="text"></label>
119
+ <label>Tier <select id="invite-tier"></select></label>
120
+ <label>Expires <input id="invite-expires" type="text" value="7d"></label>
121
+ <label>Max Calls <input id="invite-max-calls" type="number" min="1" value="100"></label>
122
+ <label>Notify <input id="invite-notify" type="text" value="all"></label>
123
+ <div class="row">
124
+ <button type="submit">Create Invite</button>
125
+ </div>
126
+ </form>
127
+
128
+ <label>Invite Message<textarea id="invite-message" rows="8" readonly></textarea></label>
129
+
130
+ <div class="row">
131
+ <h3>Existing Invites</h3>
132
+ <button id="refresh-invites">Refresh</button>
133
+ </div>
134
+ <table id="invites-table">
135
+ <thead>
136
+ <tr>
137
+ <th>ID</th>
138
+ <th>Name</th>
139
+ <th>Tier</th>
140
+ <th>Calls</th>
141
+ <th>Expires</th>
142
+ <th>Status</th>
143
+ <th>Action</th>
144
+ </tr>
145
+ </thead>
146
+ <tbody></tbody>
147
+ </table>
148
+ </section>
149
+
150
+ <div id="notice"></div>
151
+ </main>
152
+
153
+ <script src="./app.js"></script>
154
+ </body>
155
+ </html>
@@ -0,0 +1,166 @@
1
+ :root {
2
+ --bg: #f4f6f8;
3
+ --panel: #ffffff;
4
+ --ink: #13233a;
5
+ --line: #d7dee6;
6
+ --accent: #1466c1;
7
+ }
8
+
9
+ * {
10
+ box-sizing: border-box;
11
+ }
12
+
13
+ body {
14
+ margin: 0;
15
+ font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
16
+ color: var(--ink);
17
+ background: linear-gradient(180deg, #eef3f8 0%, var(--bg) 100%);
18
+ }
19
+
20
+ header {
21
+ padding: 1rem 1.5rem;
22
+ background: var(--panel);
23
+ border-bottom: 1px solid var(--line);
24
+ }
25
+
26
+ header h1 {
27
+ margin: 0;
28
+ font-size: 1.2rem;
29
+ }
30
+
31
+ header p {
32
+ margin: 0.25rem 0 0;
33
+ color: #4b5d73;
34
+ font-size: 0.9rem;
35
+ }
36
+
37
+ nav {
38
+ display: flex;
39
+ gap: 0.5rem;
40
+ padding: 0.75rem 1rem;
41
+ border-bottom: 1px solid var(--line);
42
+ background: #f8fafc;
43
+ }
44
+
45
+ .tab {
46
+ border: 1px solid var(--line);
47
+ background: #fff;
48
+ color: var(--ink);
49
+ padding: 0.45rem 0.7rem;
50
+ border-radius: 8px;
51
+ cursor: pointer;
52
+ }
53
+
54
+ .tab.is-active {
55
+ border-color: var(--accent);
56
+ color: var(--accent);
57
+ }
58
+
59
+ main {
60
+ padding: 1rem;
61
+ }
62
+
63
+ .panel {
64
+ display: none;
65
+ background: var(--panel);
66
+ border: 1px solid var(--line);
67
+ border-radius: 10px;
68
+ padding: 1rem;
69
+ }
70
+
71
+ .panel.is-active {
72
+ display: block;
73
+ }
74
+
75
+ h2,
76
+ h3 {
77
+ margin-top: 0;
78
+ }
79
+
80
+ .row {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 0.6rem;
84
+ flex-wrap: wrap;
85
+ margin-bottom: 0.8rem;
86
+ }
87
+
88
+ label {
89
+ display: block;
90
+ margin-bottom: 0.6rem;
91
+ font-size: 0.9rem;
92
+ }
93
+
94
+ input,
95
+ textarea,
96
+ select,
97
+ button {
98
+ font: inherit;
99
+ }
100
+
101
+ input,
102
+ textarea,
103
+ select {
104
+ width: 100%;
105
+ border: 1px solid var(--line);
106
+ border-radius: 8px;
107
+ padding: 0.45rem 0.55rem;
108
+ background: #fff;
109
+ }
110
+
111
+ textarea {
112
+ resize: vertical;
113
+ }
114
+
115
+ button {
116
+ border: 1px solid var(--line);
117
+ border-radius: 8px;
118
+ background: #fff;
119
+ padding: 0.42rem 0.65rem;
120
+ cursor: pointer;
121
+ }
122
+
123
+ button:hover {
124
+ border-color: var(--accent);
125
+ color: var(--accent);
126
+ }
127
+
128
+ table {
129
+ width: 100%;
130
+ border-collapse: collapse;
131
+ margin-bottom: 1rem;
132
+ }
133
+
134
+ th,
135
+ td {
136
+ border: 1px solid var(--line);
137
+ padding: 0.45rem 0.5rem;
138
+ vertical-align: top;
139
+ text-align: left;
140
+ font-size: 0.85rem;
141
+ }
142
+
143
+ th {
144
+ background: #f8fafc;
145
+ }
146
+
147
+ .summary {
148
+ max-width: 500px;
149
+ white-space: pre-wrap;
150
+ }
151
+
152
+ #notice {
153
+ margin-top: 1rem;
154
+ padding: 0.7rem 0.8rem;
155
+ border-radius: 8px;
156
+ background: #edf5ff;
157
+ border: 1px solid #c9def8;
158
+ color: #204978;
159
+ display: none;
160
+ }
161
+
162
+ @media (max-width: 720px) {
163
+ nav {
164
+ overflow-x: auto;
165
+ }
166
+ }
package/src/index.js CHANGED
@@ -17,6 +17,7 @@
17
17
  const { TokenStore } = require('./lib/tokens');
18
18
  const { A2AClient, A2AError } = require('./lib/client');
19
19
  const { createRoutes } = require('./routes/a2a');
20
+ const { createDashboardApiRouter, createDashboardUiRouter } = require('./routes/dashboard');
20
21
 
21
22
  // Lazy load optional dependencies
22
23
  let ConversationStore = null;
@@ -43,6 +44,10 @@ module.exports = {
43
44
 
44
45
  // Express routes for inbound calls
45
46
  createRoutes,
47
+
48
+ // Dashboard routes
49
+ createDashboardApiRouter,
50
+ createDashboardUiRouter,
46
51
 
47
52
  // Conversation storage (requires better-sqlite3)
48
53
  ConversationStore,