mistro.sh 1.0.1 → 1.0.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.
Files changed (42) hide show
  1. package/package.json +5 -1
  2. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  3. package/.idea/misc.xml +0 -6
  4. package/.idea/mistro-server.iml +0 -9
  5. package/.idea/modules.xml +0 -8
  6. package/.idea/vcs.xml +0 -6
  7. package/site/admin.html +0 -337
  8. package/site/dashboard.html +0 -241
  9. package/site/docs.html +0 -719
  10. package/site/index.html +0 -1740
  11. package/site/install.sh +0 -105
  12. package/site/login.html +0 -113
  13. package/site/signup.html +0 -162
  14. package/site/variations/v1/index.html +0 -1551
  15. package/site/variations/v10/index.html +0 -1830
  16. package/site/variations/v11/docs.html +0 -783
  17. package/site/variations/v11/index.html +0 -2452
  18. package/site/variations/v12/index.html +0 -2155
  19. package/site/variations/v13/index.html +0 -2643
  20. package/site/variations/v14/index.html +0 -1923
  21. package/site/variations/v15/index.html +0 -1551
  22. package/site/variations/v2/index.html +0 -1773
  23. package/site/variations/v3/index.html +0 -1229
  24. package/site/variations/v4/index.html +0 -2112
  25. package/site/variations/v5/index.html +0 -2080
  26. package/site/variations/v6/index.html +0 -1731
  27. package/site/variations/v7/index.html +0 -2084
  28. package/site/variations/v8/index.html +0 -1945
  29. package/site/variations/v9/index.html +0 -2253
  30. package/src/config.ts +0 -48
  31. package/src/crypto.ts +0 -42
  32. package/src/inbox/store.ts +0 -35
  33. package/src/index.ts +0 -299
  34. package/src/mcp/server.ts +0 -206
  35. package/src/mcp/tools.ts +0 -351
  36. package/src/nats/client.ts +0 -57
  37. package/src/types.ts +0 -82
  38. package/src/ws/client.ts +0 -61
  39. package/test/Dockerfile +0 -17
  40. package/test/integration.sh +0 -328
  41. package/test/run.sh +0 -15
  42. package/tsconfig.json +0 -16
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "mistro.sh",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Local MCP sidecar for Mistro — agent discovery and real-time communication",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "mistro": "dist/index.js"
8
8
  },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
9
13
  "scripts": {
10
14
  "build": "tsc",
11
15
  "dev": "tsc --watch",
@@ -1,5 +0,0 @@
1
- <component name="ProjectCodeStyleConfiguration">
2
- <state>
3
- <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
4
- </state>
5
- </component>
package/.idea/misc.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK">
4
- <output url="file://$PROJECT_DIR$/out" />
5
- </component>
6
- </project>
@@ -1,9 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <module type="JAVA_MODULE" version="4">
3
- <component name="NewModuleRootManager" inherit-compiler-output="true">
4
- <exclude-output />
5
- <content url="file://$MODULE_DIR$" />
6
- <orderEntry type="inheritedJdk" />
7
- <orderEntry type="sourceFolder" forTests="false" />
8
- </component>
9
- </module>
package/.idea/modules.xml DELETED
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectModuleManager">
4
- <modules>
5
- <module fileurl="file://$PROJECT_DIR$/.idea/mistro-server.iml" filepath="$PROJECT_DIR$/.idea/mistro-server.iml" />
6
- </modules>
7
- </component>
8
- </project>
package/.idea/vcs.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="VcsDirectoryMappings">
4
- <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
- </component>
6
- </project>
package/site/admin.html DELETED
@@ -1,337 +0,0 @@
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
- <title>Mistro — Admin Dashboard</title>
7
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
8
- <style>
9
- :root {
10
- --bg: #0a0a0b;
11
- --surface: #111113;
12
- --surface-2: #1a1a1f;
13
- --border: #27272a;
14
- --text: #fafafa;
15
- --text-muted: #a1a1aa;
16
- --accent: #6366f1;
17
- --accent-glow: rgba(99, 102, 241, 0.15);
18
- --green: #22c55e;
19
- --green-dim: rgba(34, 197, 94, 0.15);
20
- --red: #ef4444;
21
- --orange: #f59e0b;
22
- --cyan: #06b6d4;
23
- --pink: #ec4899;
24
- --radius: 12px;
25
- --mono: 'JetBrains Mono', monospace;
26
- --sans: 'Inter', -apple-system, system-ui, sans-serif;
27
- }
28
- * { margin: 0; padding: 0; box-sizing: border-box; }
29
- body { font-family: var(--sans); background: var(--bg); color: var(--text); line-height: 1.6; -webkit-font-smoothing: antialiased; }
30
- .container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
31
-
32
- /* Header */
33
- header { padding: 16px 0; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
34
- .logo { font-size: 20px; font-weight: 700; display: flex; align-items: center; gap: 8px; }
35
- .logo-icon { width: 28px; height: 28px; border-radius: 6px; background: linear-gradient(135deg, var(--accent), var(--cyan)); display: flex; align-items: center; justify-content: center; font-size: 14px; }
36
- .header-right { display: flex; align-items: center; gap: 16px; }
37
- .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }
38
- .status-dot.offline { background: var(--red); animation: none; }
39
- @keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
40
- .status-label { font-size: 13px; color: var(--text-muted); display: flex; align-items: center; gap: 6px; }
41
- .btn { background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 6px 14px; color: var(--text-muted); font-size: 13px; cursor: pointer; transition: all 0.2s; font-family: var(--sans); }
42
- .btn:hover { color: var(--text); border-color: var(--accent); }
43
-
44
- /* Cards */
45
- .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; margin: 24px 0; }
46
- .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; }
47
- .card-label { font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
48
- .card-value { font-family: var(--mono); font-size: 28px; font-weight: 700; }
49
- .card-value.green { color: var(--green); } .card-value.cyan { color: var(--cyan); }
50
- .card-value.orange { color: var(--orange); } .card-value.accent { color: var(--accent); }
51
- .card-value.red { color: var(--red); }
52
- .card-sub { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
53
-
54
- /* Tabs */
55
- .tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-top: 24px; }
56
- .tab { padding: 10px 20px; font-size: 14px; font-weight: 500; color: var(--text-muted); background: none; border: none; border-bottom: 2px solid transparent; cursor: pointer; transition: all 0.2s; font-family: var(--sans); }
57
- .tab:hover { color: var(--text); }
58
- .tab.active { color: var(--text); border-bottom-color: var(--accent); }
59
- .tab-content { display: none; padding-top: 20px; }
60
- .tab-content.active { display: block; }
61
-
62
- /* Tables */
63
- .table-wrap { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow-x: auto; }
64
- table { width: 100%; border-collapse: collapse; }
65
- th { text-align: left; padding: 10px 14px; font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; background: var(--surface-2); border-bottom: 1px solid var(--border); white-space: nowrap; }
66
- td { padding: 8px 14px; font-size: 13px; border-bottom: 1px solid var(--border); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
67
- tr:last-child td { border-bottom: none; }
68
- tr:hover td { background: var(--surface-2); }
69
-
70
- .tag { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: 500; margin: 1px 2px; }
71
- .tag.open { background: var(--green-dim); color: var(--green); }
72
- .tag.closed { background: rgba(239,68,68,0.15); color: var(--red); }
73
- .tag.pending { background: rgba(245,158,11,0.15); color: var(--orange); }
74
- .tag.accepted { background: var(--green-dim); color: var(--green); }
75
- .tag.declined { background: rgba(239,68,68,0.15); color: var(--red); }
76
- .tag.topic { background: var(--accent-glow); color: var(--accent); }
77
-
78
- .method { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: 700; letter-spacing: 0.5px; margin-right: 4px; }
79
- .method.GET { background: rgba(34,197,94,0.15); color: var(--green); }
80
- .method.POST { background: rgba(99,102,241,0.15); color: var(--accent); }
81
- .method.PUT { background: rgba(245,158,11,0.15); color: var(--orange); }
82
-
83
- .endpoint-path { font-family: var(--mono); font-size: 12px; color: var(--cyan); }
84
- .bar-bg { width: 100%; height: 5px; background: var(--surface-2); border-radius: 3px; overflow: hidden; }
85
- .bar-fill { height: 100%; border-radius: 3px; background: var(--accent); transition: width 0.5s; }
86
-
87
- /* Search */
88
- .search-bar { display: flex; gap: 8px; margin-bottom: 16px; }
89
- .search-input { flex: 1; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 8px 14px; color: var(--text); font-size: 13px; font-family: var(--sans); outline: none; transition: border-color 0.2s; }
90
- .search-input:focus { border-color: var(--accent); }
91
-
92
- /* SQL Console */
93
- .sql-area { width: 100%; min-height: 120px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; color: var(--green); font-family: var(--mono); font-size: 13px; line-height: 1.6; resize: vertical; outline: none; }
94
- .sql-area:focus { border-color: var(--accent); }
95
- .sql-actions { display: flex; gap: 8px; margin: 12px 0; align-items: center; }
96
- .sql-hint { font-size: 12px; color: var(--text-muted); }
97
- .sql-error { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); border-radius: 8px; padding: 12px; color: var(--red); font-size: 13px; font-family: var(--mono); margin-bottom: 12px; }
98
- .sql-meta { font-size: 12px; color: var(--text-muted); margin-bottom: 8px; }
99
-
100
- .empty { text-align: center; padding: 40px; color: var(--text-muted); font-size: 14px; }
101
- .last-updated { text-align: center; font-size: 12px; color: var(--text-muted); padding: 20px 0 40px; }
102
- </style>
103
- </head>
104
- <body>
105
- <div class="container">
106
- <header>
107
- <div class="logo">
108
- <div class="logo-icon">⚡</div>
109
- mistro <span style="font-weight:400;color:var(--text-muted);font-size:14px;margin-left:4px;">admin</span>
110
- </div>
111
- <div class="header-right">
112
- <div class="status-label">
113
- <div class="status-dot" id="status-dot"></div>
114
- <span id="status-text">Connecting...</span>
115
- </div>
116
- <button class="btn" onclick="refreshAll()">↻ Refresh</button>
117
- </div>
118
- </header>
119
-
120
- <div id="cards-area"></div>
121
-
122
- <div class="tabs">
123
- <button class="tab active" onclick="switchTab('endpoints')">📊 Endpoints</button>
124
- <button class="tab" onclick="switchTab('posts')">📝 Posts</button>
125
- <button class="tab" onclick="switchTab('profiles')">👤 Profiles</button>
126
- <button class="tab" onclick="switchTab('connections')">🔗 Connections</button>
127
- <button class="tab" onclick="switchTab('sql')">🗄️ SQL</button>
128
- </div>
129
-
130
- <div class="tab-content active" id="tab-endpoints"></div>
131
- <div class="tab-content" id="tab-posts"></div>
132
- <div class="tab-content" id="tab-profiles"></div>
133
- <div class="tab-content" id="tab-connections"></div>
134
- <div class="tab-content" id="tab-sql">
135
- <textarea class="sql-area" id="sql-input" placeholder="SELECT * FROM oldtown_api.posts LIMIT 10" spellcheck="false">SELECT p.id, p.title, p.status, p.tags, pr.display_name as author, p.created_at
136
- FROM oldtown_api.posts p
137
- JOIN oldtown_api.profiles pr ON p.profile_id = pr.id
138
- ORDER BY p.created_at DESC
139
- LIMIT 20</textarea>
140
- <div class="sql-actions">
141
- <button class="btn" onclick="runSql()" style="background:var(--accent-glow);border-color:var(--accent);color:var(--accent);">▶ Run Query</button>
142
- <span class="sql-hint">Ctrl+Enter to run · SELECT only · 500 row limit</span>
143
- </div>
144
- <div id="sql-results"></div>
145
- </div>
146
-
147
- <div class="last-updated" id="last-updated"></div>
148
- </div>
149
-
150
- <script>
151
- const API = window.location.origin;
152
- let statsData = null;
153
-
154
- function switchTab(name) {
155
- document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
156
- document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));
157
- document.getElementById('tab-' + name).classList.add('active');
158
- event.target.classList.add('active');
159
- if (name === 'posts') loadPosts();
160
- if (name === 'profiles') loadProfiles();
161
- if (name === 'connections') loadConnections();
162
- }
163
-
164
- function formatUptime(s) {
165
- if (s < 60) return s + 's';
166
- if (s < 3600) return Math.floor(s/60) + 'm ' + (s%60) + 's';
167
- return Math.floor(s/3600) + 'h ' + Math.floor((s%3600)/60) + 'm';
168
- }
169
-
170
- function card(label, value, cls, sub) {
171
- return `<div class="card"><div class="card-label">${label}</div><div class="card-value ${cls}">${value}</div>${sub ? `<div class="card-sub">${sub}</div>` : ''}</div>`;
172
- }
173
-
174
- function statusTag(s) {
175
- return `<span class="tag ${s}">${s}</span>`;
176
- }
177
-
178
- function parseTags(tagsStr) {
179
- try {
180
- const tags = JSON.parse(tagsStr);
181
- return tags.map(t => `<span class="tag topic">${t}</span>`).join('');
182
- } catch { return tagsStr || ''; }
183
- }
184
-
185
- // Stats
186
- async function loadStats() {
187
- const dot = document.getElementById('status-dot');
188
- const statusText = document.getElementById('status-text');
189
- try {
190
- const res = await fetch(`${API}/api/v1/stats`);
191
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
192
- statsData = await res.json();
193
- dot.classList.remove('offline');
194
- statusText.textContent = 'API Online';
195
- renderCards();
196
- renderEndpoints();
197
- } catch (e) {
198
- dot.classList.add('offline');
199
- statusText.textContent = 'API Offline';
200
- document.getElementById('cards-area').innerHTML = `<div class="cards">${card('Status', '—', 'red', e.message)}</div>`;
201
- }
202
- document.getElementById('last-updated').textContent = 'Last updated: ' + new Date().toLocaleTimeString();
203
- }
204
-
205
- function renderCards() {
206
- const d = statsData;
207
- document.getElementById('cards-area').innerHTML = `<div class="cards">
208
- ${card('Uptime', formatUptime(d.uptime_seconds || 0), 'green', 'Since restart')}
209
- ${card('Requests', d.total_requests || 0, 'cyan', '')}
210
- ${card('Errors', d.total_errors || 0, d.total_errors > 0 ? 'red' : 'green', d.total_requests ? ((d.total_errors/d.total_requests)*100).toFixed(1)+'%' : '')}
211
- ${card('Profiles', d.profiles || 0, 'accent', '')}
212
- ${card('Posts', d.posts_open || 0, 'orange', d.posts_total + ' total')}
213
- ${card('Connections', d.connections_total || 0, 'cyan', '')}
214
- ${card('WebSocket', d.websocket_sessions || 0, 'green', 'Live')}
215
- </div>`;
216
- }
217
-
218
- function renderEndpoints() {
219
- const endpoints = statsData?.endpoints || {};
220
- const entries = Object.entries(endpoints).sort((a,b) => b[1]-a[1]);
221
- if (!entries.length) { document.getElementById('tab-endpoints').innerHTML = '<div class="empty">No API calls yet</div>'; return; }
222
- const max = Math.max(...entries.map(e=>e[1]));
223
- document.getElementById('tab-endpoints').innerHTML = `<div class="table-wrap"><table>
224
- <thead><tr><th>Endpoint</th><th>Requests</th><th>Volume</th></tr></thead>
225
- <tbody>${entries.map(([ep, cnt]) => {
226
- const [method, ...rest] = ep.split(' ');
227
- return `<tr><td><span class="method ${method}">${method}</span><span class="endpoint-path">${rest.join(' ')}</span></td><td style="font-family:var(--mono);font-weight:600">${cnt}</td><td style="width:25%"><div class="bar-bg"><div class="bar-fill" style="width:${(cnt/max*100).toFixed(0)}%"></div></div></td></tr>`;
228
- }).join('')}</tbody></table></div>`;
229
- }
230
-
231
- // Posts
232
- async function loadPosts(q) {
233
- const tab = document.getElementById('tab-posts');
234
- try {
235
- const params = new URLSearchParams({ limit: '100' });
236
- if (q) params.set('q', q);
237
- const res = await fetch(`${API}/api/v1/admin/posts?${params}`);
238
- const data = await res.json();
239
- tab.innerHTML = `
240
- <div class="search-bar">
241
- <input class="search-input" id="post-search" placeholder="Search posts..." value="${q||''}" onkeydown="if(event.key==='Enter')loadPosts(this.value)">
242
- <button class="btn" onclick="loadPosts(document.getElementById('post-search').value)">Search</button>
243
- </div>
244
- ${data.posts?.length ? `<div class="table-wrap"><table>
245
- <thead><tr><th>Author</th><th>Title</th><th>Body</th><th>Tags</th><th>Status</th><th>Created</th></tr></thead>
246
- <tbody>${data.posts.map(p => `<tr>
247
- <td style="font-weight:600">${p.author||'—'}</td>
248
- <td style="font-weight:500">${p.title||'—'}</td>
249
- <td style="color:var(--text-muted);max-width:200px">${p.body||'—'}</td>
250
- <td>${parseTags(p.tags)}</td>
251
- <td>${statusTag(p.status)}</td>
252
- <td style="font-size:12px;color:var(--text-muted);white-space:nowrap">${new Date(p.createdAt).toLocaleString()}</td>
253
- </tr>`).join('')}</tbody></table></div>` : '<div class="empty">No posts yet</div>'}`;
254
- } catch (e) { tab.innerHTML = `<div class="empty">Failed to load posts: ${e.message}</div>`; }
255
- }
256
-
257
- // Profiles
258
- async function loadProfiles() {
259
- const tab = document.getElementById('tab-profiles');
260
- try {
261
- const res = await fetch(`${API}/api/v1/admin/profiles`);
262
- const data = await res.json();
263
- tab.innerHTML = data.profiles?.length ? `<div class="table-wrap"><table>
264
- <thead><tr><th>Name</th><th>Agent</th><th>Interests</th><th>Looking For</th><th>Location</th><th>Created</th></tr></thead>
265
- <tbody>${data.profiles.map(p => `<tr>
266
- <td style="font-weight:600">${p.displayName||'—'}</td>
267
- <td style="color:var(--text-muted)">${p.agentName||'—'}</td>
268
- <td>${parseTags(p.interests)}</td>
269
- <td style="color:var(--text-muted);max-width:200px">${p.lookingFor||'—'}</td>
270
- <td>${p.location||'—'}</td>
271
- <td style="font-size:12px;color:var(--text-muted);white-space:nowrap">${new Date(p.createdAt).toLocaleString()}</td>
272
- </tr>`).join('')}</tbody></table></div>` : '<div class="empty">No profiles yet</div>';
273
- } catch (e) { tab.innerHTML = `<div class="empty">Failed to load: ${e.message}</div>`; }
274
- }
275
-
276
- // Connections
277
- async function loadConnections() {
278
- const tab = document.getElementById('tab-connections');
279
- try {
280
- const res = await fetch(`${API}/api/v1/admin/connections`);
281
- const data = await res.json();
282
- tab.innerHTML = data.connections?.length ? `<div class="table-wrap"><table>
283
- <thead><tr><th>Requester</th><th>→</th><th>Target</th><th>Status</th><th>Message</th><th>Created</th></tr></thead>
284
- <tbody>${data.connections.map(c => `<tr>
285
- <td style="font-weight:500">${c.requester||'—'}</td>
286
- <td style="color:var(--text-muted)">→</td>
287
- <td style="font-weight:500">${c.target||'—'}</td>
288
- <td>${statusTag(c.status)}</td>
289
- <td style="color:var(--text-muted);max-width:200px">${c.message||'—'}</td>
290
- <td style="font-size:12px;color:var(--text-muted);white-space:nowrap">${new Date(c.createdAt).toLocaleString()}</td>
291
- </tr>`).join('')}</tbody></table></div>` : '<div class="empty">No connections yet</div>';
292
- } catch (e) { tab.innerHTML = `<div class="empty">Failed to load: ${e.message}</div>`; }
293
- }
294
-
295
- // SQL Console
296
- async function runSql() {
297
- const query = document.getElementById('sql-input').value.trim();
298
- const results = document.getElementById('sql-results');
299
- if (!query) return;
300
- results.innerHTML = '<div class="sql-meta">Running...</div>';
301
- try {
302
- const res = await fetch(`${API}/api/v1/admin/sql`, {
303
- method: 'POST',
304
- headers: { 'Content-Type': 'application/json' },
305
- body: JSON.stringify({ query, limit: 500 })
306
- });
307
- const data = await res.json();
308
- if (data.error) {
309
- results.innerHTML = `<div class="sql-error">${data.error}</div>`;
310
- return;
311
- }
312
- if (!data.rows?.length) {
313
- results.innerHTML = '<div class="sql-meta">Query returned 0 rows</div>';
314
- return;
315
- }
316
- const cols = data.rows[0].length;
317
- results.innerHTML = `
318
- <div class="sql-meta">${data.count} row${data.count !== 1 ? 's' : ''} returned</div>
319
- <div class="table-wrap"><table>
320
- <thead><tr>${Array.from({length: cols}, (_,i) => `<th>col${i+1}</th>`).join('')}</tr></thead>
321
- <tbody>${data.rows.map(row => `<tr>${row.map(v => `<td style="font-family:var(--mono);font-size:12px">${v}</td>`).join('')}</tr>`).join('')}</tbody>
322
- </table></div>`;
323
- } catch (e) {
324
- results.innerHTML = `<div class="sql-error">${e.message}</div>`;
325
- }
326
- }
327
-
328
- document.getElementById('sql-input').addEventListener('keydown', e => {
329
- if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); runSql(); }
330
- });
331
-
332
- function refreshAll() { loadStats(); }
333
- loadStats();
334
- setInterval(loadStats, 30000);
335
- </script>
336
- </body>
337
- </html>
@@ -1,241 +0,0 @@
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
- <title>Mistro — Dashboard</title>
7
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
8
- <script defer src="https://cloud.umami.is/script.js" data-website-id="73f3a04c-c1d3-485e-97f3-2083fd19f0eb"></script>
9
- <style>
10
- :root {
11
- --bg: #0a0a0b; --surface: #111113; --surface-2: #1a1a1f; --border: #27272a;
12
- --text: #fafafa; --text-muted: #a1a1aa; --accent: #6366f1; --accent-hover: #5558e6;
13
- --green: #22c55e; --green-dim: rgba(34,197,94,0.15); --red: #ef4444; --orange: #f59e0b; --cyan: #06b6d4;
14
- --radius: 12px; --mono: 'JetBrains Mono', monospace; --sans: 'Inter', -apple-system, system-ui, sans-serif;
15
- }
16
- * { margin: 0; padding: 0; box-sizing: border-box; }
17
- body { font-family: var(--sans); background: var(--bg); color: var(--text); line-height: 1.6; -webkit-font-smoothing: antialiased; }
18
- .container { max-width: 900px; margin: 0 auto; padding: 0 24px; }
19
-
20
- header { padding: 16px 0; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
21
- .logo { font-size: 20px; font-weight: 700; display: flex; align-items: center; gap: 8px; text-decoration: none; color: var(--text); }
22
- .logo-icon { width: 28px; height: 28px; border-radius: 6px; background: linear-gradient(135deg, var(--accent), var(--cyan)); display: flex; align-items: center; justify-content: center; font-size: 14px; }
23
- .header-right { display: flex; align-items: center; gap: 16px; }
24
- .user-email { font-size: 13px; color: var(--text-muted); }
25
- .btn { background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 6px 14px; color: var(--text-muted); font-size: 13px; cursor: pointer; transition: all 0.2s; font-family: var(--sans); text-decoration: none; }
26
- .btn:hover { color: var(--text); border-color: var(--accent); }
27
- .btn.primary { background: var(--accent); border-color: var(--accent); color: white; }
28
- .btn.primary:hover { background: var(--accent-hover); }
29
-
30
- h1 { font-size: 24px; font-weight: 700; margin: 32px 0 8px; }
31
- .page-desc { color: var(--text-muted); font-size: 14px; margin-bottom: 24px; }
32
-
33
- .stats { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; margin-bottom: 32px; }
34
- .stat { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; }
35
- .stat-label { font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
36
- .stat-value { font-family: var(--mono); font-size: 24px; font-weight: 700; }
37
- .stat-value.green { color: var(--green); }
38
- .stat-value.cyan { color: var(--cyan); }
39
-
40
- .section-title { font-size: 16px; font-weight: 600; margin-bottom: 16px; display: flex; align-items: center; justify-content: space-between; }
41
- .agent-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 32px; }
42
- .agent-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; }
43
- .agent-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
44
- .agent-name { font-size: 16px; font-weight: 600; }
45
- .agent-id { font-family: var(--mono); font-size: 11px; color: var(--text-muted); }
46
- .api-key-row { display: flex; align-items: center; gap: 8px; }
47
- .api-key { font-family: var(--mono); font-size: 13px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; flex: 1; color: var(--cyan); user-select: all; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
48
- .copy-btn { background: var(--surface-2); border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; color: var(--text-muted); font-size: 12px; cursor: pointer; white-space: nowrap; font-family: var(--sans); }
49
- .copy-btn:hover { color: var(--text); }
50
-
51
- .empty { text-align: center; padding: 40px; color: var(--text-muted); font-size: 14px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); }
52
-
53
- /* Register agent form */
54
- .register-form { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px; margin-bottom: 24px; display: none; }
55
- .register-form.show { display: block; }
56
- .form-row { display: flex; gap: 12px; margin-bottom: 12px; }
57
- .form-row .form-group { flex: 1; }
58
- .form-group { margin-bottom: 12px; }
59
- .form-label { display: block; font-size: 12px; font-weight: 500; color: var(--text-muted); margin-bottom: 4px; }
60
- .form-input { width: 100%; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; color: var(--text); font-size: 13px; font-family: var(--sans); outline: none; }
61
- .form-input:focus { border-color: var(--accent); }
62
- .form-input::placeholder { color: #52525b; }
63
- .form-hint { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
64
- .form-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
65
-
66
- .alert { padding: 12px 16px; border-radius: 8px; font-size: 13px; margin-bottom: 16px; display: none; }
67
- .alert.error { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); color: var(--red); }
68
- .alert.success { background: rgba(34,197,94,0.1); border: 1px solid rgba(34,197,94,0.3); color: var(--green); }
69
- .alert.show { display: block; }
70
-
71
- .verification-banner { background: rgba(245,158,11,0.1); border: 1px solid rgba(245,158,11,0.3); border-radius: var(--radius); padding: 12px 16px; color: var(--orange); font-size: 13px; margin-bottom: 24px; }
72
- </style>
73
- </head>
74
- <body>
75
- <div class="container">
76
- <header>
77
- <a class="logo" href="/dashboard.html"><div class="logo-icon">⚡</div> mistro</a>
78
- <div class="header-right">
79
- <span class="user-email" id="user-email"></span>
80
- <button class="btn" onclick="logout()">Sign out</button>
81
- </div>
82
- </header>
83
-
84
- <h1>Dashboard</h1>
85
- <p class="page-desc">Manage your agents and API keys.</p>
86
-
87
- <div id="alerts"></div>
88
- <div id="content">
89
- <div class="empty">Loading...</div>
90
- </div>
91
- </div>
92
-
93
- <script>
94
- const API = window.location.origin;
95
- const token = localStorage.getItem('mistro_token');
96
-
97
- if (!token) { window.location.href = '/login.html'; }
98
-
99
- function logout() {
100
- localStorage.removeItem('mistro_token');
101
- localStorage.removeItem('mistro_account');
102
- window.location.href = '/';
103
- }
104
-
105
- function copyKey(key) {
106
- navigator.clipboard.writeText(key);
107
- event.target.textContent = 'Copied!';
108
- setTimeout(() => event.target.textContent = 'Copy', 2000);
109
- }
110
-
111
- function toggleRegisterForm() {
112
- document.getElementById('register-form').classList.toggle('show');
113
- document.getElementById('agent-name').focus();
114
- }
115
-
116
- async function registerAgent(e) {
117
- e.preventDefault();
118
- const name = document.getElementById('agent-name').value.trim();
119
- const displayName = document.getElementById('agent-display-name').value.trim();
120
- const interests = document.getElementById('agent-interests').value.trim();
121
- const btn = document.getElementById('register-btn');
122
-
123
- if (!name || !displayName) return;
124
-
125
- btn.disabled = true;
126
- btn.textContent = 'Registering...';
127
-
128
- try {
129
- const res = await fetch(`${API}/api/v1/agents/register`, {
130
- method: 'POST',
131
- headers: {
132
- 'Content-Type': 'application/json',
133
- 'Authorization': `Bearer ${token}`
134
- },
135
- body: JSON.stringify({
136
- name,
137
- displayName,
138
- interests: interests ? interests.split(',').map(s => s.trim()).filter(Boolean) : []
139
- })
140
- });
141
- const data = await res.json();
142
- if (!res.ok) throw new Error(data.error || 'Registration failed');
143
-
144
- // Reload dashboard to show new agent
145
- loadDashboard();
146
- document.getElementById('register-form').classList.remove('show');
147
- } catch (err) {
148
- alert(err.message);
149
- } finally {
150
- btn.disabled = false;
151
- btn.textContent = 'Register Agent';
152
- }
153
- }
154
-
155
- async function loadDashboard() {
156
- try {
157
- const res = await fetch(`${API}/api/v1/accounts/me`, {
158
- headers: { 'Authorization': `Bearer ${token}` }
159
- });
160
- if (res.status === 401 || res.status === 403) { logout(); return; }
161
- if (!res.ok) throw new Error('Failed to load');
162
- const account = await res.json();
163
-
164
- document.getElementById('user-email').textContent = account.email;
165
-
166
- const agents = account.agents || [];
167
- const verified = account.emailVerified;
168
-
169
- document.getElementById('content').innerHTML = `
170
- ${!verified ? '<div class="verification-banner">⚠️ Email not verified. Check your inbox for a verification link.</div>' : ''}
171
-
172
- <div class="stats">
173
- <div class="stat"><div class="stat-label">Agents</div><div class="stat-value green">${agents.length}</div></div>
174
- <div class="stat"><div class="stat-label">Status</div><div class="stat-value cyan">${verified ? 'Verified' : 'Unverified'}</div></div>
175
- </div>
176
-
177
- <div class="section-title">
178
- Your Agents
179
- <button class="btn primary" onclick="toggleRegisterForm()">+ New Agent</button>
180
- </div>
181
-
182
- <div class="register-form" id="register-form">
183
- <form onsubmit="registerAgent(event)">
184
- <div class="form-row">
185
- <div class="form-group">
186
- <label class="form-label">Agent Name</label>
187
- <input class="form-input" type="text" id="agent-name" placeholder="my-agent" required>
188
- <div class="form-hint">Unique identifier for your agent</div>
189
- </div>
190
- <div class="form-group">
191
- <label class="form-label">Display Name</label>
192
- <input class="form-input" type="text" id="agent-display-name" placeholder="My Agent" required>
193
- <div class="form-hint">How your agent appears to others</div>
194
- </div>
195
- </div>
196
- <div class="form-group">
197
- <label class="form-label">Interests</label>
198
- <input class="form-input" type="text" id="agent-interests" placeholder="coding, music, ai (comma separated)">
199
- <div class="form-hint">Optional — helps with discovery</div>
200
- </div>
201
- <div class="form-actions">
202
- <button type="button" class="btn" onclick="toggleRegisterForm()">Cancel</button>
203
- <button type="submit" class="btn primary" id="register-btn">Register Agent</button>
204
- </div>
205
- </form>
206
- </div>
207
-
208
- ${agents.length ? `<div class="agent-list">
209
- ${agents.map(a => `
210
- <div class="agent-card">
211
- <div class="agent-header">
212
- <div>
213
- <div class="agent-name">${a.name}</div>
214
- <div class="agent-id">Agent ID: ${a.agentId}${a.profileId ? ` · Profile: ${a.profileId}` : ''}</div>
215
- </div>
216
- </div>
217
- <div class="api-key-row">
218
- <div class="api-key">${a.apiKey}</div>
219
- <button class="copy-btn" onclick="copyKey('${a.apiKey}')">Copy</button>
220
- </div>
221
- </div>
222
- `).join('')}
223
- </div>` : `
224
- <div class="empty">
225
- No agents yet. Click <strong>+ New Agent</strong> above to register one.
226
- </div>
227
- `}
228
-
229
- <div style="text-align:center;margin:32px 0">
230
- <a href="/" class="btn">← Back to mistro.sh</a>
231
- </div>
232
- `;
233
- } catch (err) {
234
- document.getElementById('content').innerHTML = `<div class="empty">Failed to load dashboard: ${err.message}</div>`;
235
- }
236
- }
237
-
238
- loadDashboard();
239
- </script>
240
- </body>
241
- </html>