myceliumail 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.
- package/package.json +3 -3
- package/.context7 +0 -87
- package/.eslintrc.json +0 -29
- package/COMPLETE.md +0 -51
- package/MYCELIUMAIL_STARTER_KIT.md +0 -603
- package/NEXT_STEPS.md +0 -96
- package/desktop/README.md +0 -102
- package/desktop/assets/icon.icns +0 -0
- package/desktop/assets/icon.iconset/icon_128x128.png +0 -0
- package/desktop/assets/icon.iconset/icon_128x128@2x.png +0 -0
- package/desktop/assets/icon.iconset/icon_16x16.png +0 -0
- package/desktop/assets/icon.iconset/icon_16x16@2x.png +0 -0
- package/desktop/assets/icon.iconset/icon_256x256.png +0 -0
- package/desktop/assets/icon.iconset/icon_256x256@2x.png +0 -0
- package/desktop/assets/icon.iconset/icon_32x32.png +0 -0
- package/desktop/assets/icon.iconset/icon_32x32@2x.png +0 -0
- package/desktop/assets/icon.iconset/icon_512x512.png +0 -0
- package/desktop/assets/icon.iconset/icon_512x512@2x.png +0 -0
- package/desktop/assets/icon.png +0 -0
- package/desktop/assets/tray-icon.png +0 -0
- package/desktop/main.js +0 -257
- package/desktop/package-lock.json +0 -4198
- package/desktop/package.json +0 -48
- package/desktop/preload.js +0 -11
- package/docs/20251215_Treebird-Ecosystem_Knowledge-Base_v2.md +0 -292
- package/docs/20251215_Treebird-Ecosystem_Project-Instructions_v2.md +0 -176
- package/docs/AGENT_DELEGATION_WORKFLOW.md +0 -453
- package/docs/AGENT_STARTER_KIT.md +0 -145
- package/docs/ANNOUNCEMENT_DRAFTS.md +0 -55
- package/docs/DASHBOARD_AGENT_HANDOFF.md +0 -429
- package/docs/DASHBOARD_AGENT_PROMPT.md +0 -32
- package/docs/DASHBOARD_BUILD_ROADMAP.md +0 -61
- package/docs/DEPLOYMENT.md +0 -59
- package/docs/MCP_PUBLISHING_ROADMAP.md +0 -113
- package/docs/MCP_STARTER_KIT.md +0 -117
- package/docs/SSAN_MESSAGES_SUMMARY.md +0 -92
- package/docs/STORAGE_ARCHITECTURE.md +0 -114
- package/mcp-server/README.md +0 -143
- package/mcp-server/assets/icon.png +0 -0
- package/mcp-server/myceliumail-mcp-1.0.0.tgz +0 -0
- package/mcp-server/package-lock.json +0 -1142
- package/mcp-server/package.json +0 -49
- package/mcp-server/src/lib/config.ts +0 -21
- package/mcp-server/src/lib/crypto.ts +0 -150
- package/mcp-server/src/lib/storage.ts +0 -257
- package/mcp-server/src/server.ts +0 -387
- package/mcp-server/tsconfig.json +0 -26
- package/src/bin/myceliumail.ts +0 -52
- package/src/commands/broadcast.ts +0 -70
- package/src/commands/dashboard.ts +0 -19
- package/src/commands/inbox.ts +0 -75
- package/src/commands/key-import.ts +0 -35
- package/src/commands/keygen.ts +0 -44
- package/src/commands/keys.ts +0 -55
- package/src/commands/read.ts +0 -97
- package/src/commands/send.ts +0 -89
- package/src/commands/watch.ts +0 -101
- package/src/dashboard/public/app.js +0 -523
- package/src/dashboard/public/index.html +0 -75
- package/src/dashboard/public/styles.css +0 -68
- package/src/dashboard/routes.ts +0 -128
- package/src/dashboard/server.ts +0 -33
- package/src/lib/config.ts +0 -104
- package/src/lib/crypto.ts +0 -210
- package/src/lib/realtime.ts +0 -109
- package/src/storage/local.ts +0 -209
- package/src/storage/supabase.ts +0 -336
- package/src/types/index.ts +0 -53
- package/supabase/migrations/000_myceliumail_setup.sql +0 -93
- package/supabase/migrations/001_enable_realtime.sql +0 -10
- package/tsconfig.json +0 -28
|
@@ -1,523 +0,0 @@
|
|
|
1
|
-
// Load inbox on page load
|
|
2
|
-
let currentMessageId = null;
|
|
3
|
-
let currentAgentId = 'treebird'; // Default viewer identity
|
|
4
|
-
let supabaseClient = null;
|
|
5
|
-
let realtimeChannel = null;
|
|
6
|
-
|
|
7
|
-
// Request notification permission on load
|
|
8
|
-
async function requestNotificationPermission() {
|
|
9
|
-
if ('Notification' in window && Notification.permission === 'default') {
|
|
10
|
-
await Notification.requestPermission();
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Show browser notification
|
|
15
|
-
function showBrowserNotification(message) {
|
|
16
|
-
if ('Notification' in window && Notification.permission === 'granted') {
|
|
17
|
-
const preview = message.message?.substring(0, 100) || message.body?.substring(0, 100) || '';
|
|
18
|
-
const notification = new Notification(`📬 ${message.from_agent}: ${message.subject}`, {
|
|
19
|
-
body: preview,
|
|
20
|
-
icon: '🍄',
|
|
21
|
-
tag: message.id, // Prevent duplicate notifications
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
notification.onclick = () => {
|
|
25
|
-
window.focus();
|
|
26
|
-
viewMessage(message.id);
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Setup Supabase Realtime subscription
|
|
32
|
-
async function setupRealtime() {
|
|
33
|
-
try {
|
|
34
|
-
// Fetch config from server
|
|
35
|
-
const res = await fetch('/api/config');
|
|
36
|
-
const config = await res.json();
|
|
37
|
-
|
|
38
|
-
if (!config.supabaseUrl || !config.supabaseKey) {
|
|
39
|
-
console.log('Supabase not configured, using polling only');
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const agentId = config.agentId || 'anonymous';
|
|
44
|
-
currentAgentId = agentId;
|
|
45
|
-
console.log('🍄 Setting up Realtime for', agentId);
|
|
46
|
-
|
|
47
|
-
supabaseClient = supabase.createClient(config.supabaseUrl, config.supabaseKey);
|
|
48
|
-
|
|
49
|
-
realtimeChannel = supabaseClient
|
|
50
|
-
.channel('dashboard-inbox')
|
|
51
|
-
.on(
|
|
52
|
-
'postgres_changes',
|
|
53
|
-
{
|
|
54
|
-
event: 'INSERT',
|
|
55
|
-
schema: 'public',
|
|
56
|
-
table: 'agent_messages',
|
|
57
|
-
filter: `to_agent=eq.${agentId}`
|
|
58
|
-
},
|
|
59
|
-
(payload) => {
|
|
60
|
-
console.log('📬 New message via Realtime:', payload.new.subject);
|
|
61
|
-
showBrowserNotification(payload.new);
|
|
62
|
-
loadInbox(true); // Refresh inbox to show new message
|
|
63
|
-
}
|
|
64
|
-
)
|
|
65
|
-
.subscribe((status) => {
|
|
66
|
-
if (status === 'SUBSCRIBED') {
|
|
67
|
-
console.log('✅ Connected to Supabase Realtime');
|
|
68
|
-
// Update UI to show we're connected
|
|
69
|
-
const stats = document.getElementById('stats');
|
|
70
|
-
if (stats && !stats.innerHTML.includes('🔴')) {
|
|
71
|
-
stats.innerHTML = stats.innerHTML.replace('Loading...', '<span class="text-green-400">●</span> ' + stats.innerHTML);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
} catch (err) {
|
|
76
|
-
console.error('Failed to setup Realtime:', err);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function loadInbox(preserveSelection = true) {
|
|
81
|
-
try {
|
|
82
|
-
const res = await fetch('/api/inbox');
|
|
83
|
-
const data = await res.json();
|
|
84
|
-
|
|
85
|
-
updateInboxList(data.messages);
|
|
86
|
-
updateStats(data);
|
|
87
|
-
|
|
88
|
-
// If we have a selected message, make sure it's highlighted/marked as active
|
|
89
|
-
if (preserveSelection && currentMessageId) {
|
|
90
|
-
highlightMessage(currentMessageId);
|
|
91
|
-
}
|
|
92
|
-
} catch (err) {
|
|
93
|
-
console.error('Failed to load inbox:', err);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function updateInboxList(messages) {
|
|
98
|
-
const list = document.getElementById('inbox-list');
|
|
99
|
-
|
|
100
|
-
if (messages.length === 0) {
|
|
101
|
-
list.innerHTML = `
|
|
102
|
-
<div class="text-center text-gray-600 py-8">
|
|
103
|
-
No messages found
|
|
104
|
-
</div>
|
|
105
|
-
`;
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Sort by timestamp descending
|
|
110
|
-
messages.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
111
|
-
|
|
112
|
-
list.innerHTML = messages.map(msg => {
|
|
113
|
-
const isUnread = !msg.read && (!msg.readBy || !msg.readBy.includes(currentAgentId));
|
|
114
|
-
return `
|
|
115
|
-
<div id="msg-${msg.id}"
|
|
116
|
-
class="message-item p-4 rounded-lg cursor-pointer border border-transparent hover:border-gray-700 hover:bg-gray-800 transition-all group ${isUnread ? 'bg-gray-800/50 border-gray-700' : ''}"
|
|
117
|
-
onclick="viewMessage('${msg.id}')">
|
|
118
|
-
<div class="flex items-center justify-between mb-1">
|
|
119
|
-
<div class="flex items-center gap-2">
|
|
120
|
-
${msg.encrypted ? '<span title="Encrypted">🔐</span>' : '<span title="Cleartext">📨</span>'}
|
|
121
|
-
<span class="font-medium text-gray-200 ${isUnread ? 'text-white' : ''}">${msg.sender}</span>
|
|
122
|
-
<span class="text-gray-500 text-sm">→ ${msg.recipient}</span>
|
|
123
|
-
</div>
|
|
124
|
-
${isUnread ? '<span class="w-2 h-2 rounded-full bg-blue-500"></span>' : ''}
|
|
125
|
-
</div>
|
|
126
|
-
<div class="text-base font-medium ${isUnread ? 'text-gray-100' : 'text-gray-400'} truncate mb-0.5">
|
|
127
|
-
${msg.subject || '(no subject)'}
|
|
128
|
-
</div>
|
|
129
|
-
<div class="text-sm text-gray-500">
|
|
130
|
-
${new Date(msg.createdAt).toLocaleString()}
|
|
131
|
-
</div>
|
|
132
|
-
</div>
|
|
133
|
-
`}).join('');
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function viewMessage(id) {
|
|
137
|
-
currentMessageId = id;
|
|
138
|
-
highlightMessage(id);
|
|
139
|
-
|
|
140
|
-
// Show loading state
|
|
141
|
-
const detail = document.getElementById('message-detail');
|
|
142
|
-
detail.innerHTML = '<div class="h-full flex items-center justify-center text-gray-500">Loading...</div>';
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
const res = await fetch(`/api/message/${id}`);
|
|
146
|
-
const msg = await res.json();
|
|
147
|
-
|
|
148
|
-
renderMessageDetail(msg);
|
|
149
|
-
|
|
150
|
-
// Mark as read for current agent
|
|
151
|
-
if (!msg.readBy || !msg.readBy.includes(currentAgentId)) {
|
|
152
|
-
await fetch(`/api/message/${id}/read`, {
|
|
153
|
-
method: 'POST',
|
|
154
|
-
headers: { 'Content-Type': 'application/json' },
|
|
155
|
-
body: JSON.stringify({ readerId: currentAgentId })
|
|
156
|
-
});
|
|
157
|
-
// Update local UI state immediately
|
|
158
|
-
const item = document.getElementById(`msg-${id}`);
|
|
159
|
-
if (item) {
|
|
160
|
-
const indicator = item.querySelector('.bg-blue-500');
|
|
161
|
-
if (indicator) indicator.remove();
|
|
162
|
-
item.classList.remove('bg-gray-800/50', 'border-gray-700');
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
} catch (err) {
|
|
167
|
-
console.error('Failed to view message:', err);
|
|
168
|
-
detail.innerHTML = '<div class="text-red-500">Failed to load message</div>';
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function highlightMessage(id) {
|
|
173
|
-
// Remove active class from all
|
|
174
|
-
document.querySelectorAll('.message-item').forEach(el => {
|
|
175
|
-
el.classList.remove('ring-1', 'ring-purple-500', 'bg-gray-800');
|
|
176
|
-
});
|
|
177
|
-
// Add to current
|
|
178
|
-
const el = document.getElementById(`msg-${id}`);
|
|
179
|
-
if (el) {
|
|
180
|
-
el.classList.add('ring-1', 'ring-purple-500', 'bg-gray-800');
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function renderMessageDetail(msg) {
|
|
185
|
-
const detail = document.getElementById('message-detail');
|
|
186
|
-
|
|
187
|
-
detail.innerHTML = `
|
|
188
|
-
<div class="max-w-4xl mx-auto w-full anim-fade-in">
|
|
189
|
-
<div class="mb-8 p-6 bg-gray-850 rounded-xl border border-gray-800">
|
|
190
|
-
<div class="flex justify-between items-start mb-4">
|
|
191
|
-
<div>
|
|
192
|
-
<div class="text-base text-gray-400 mb-1">From</div>
|
|
193
|
-
<div class="font-mono text-xl text-blue-400">${msg.sender}</div>
|
|
194
|
-
</div>
|
|
195
|
-
<div class="text-center">
|
|
196
|
-
<div class="text-base text-gray-400 mb-1">To</div>
|
|
197
|
-
<div class="font-mono text-xl text-green-400">${msg.recipient}</div>
|
|
198
|
-
</div>
|
|
199
|
-
<div class="text-right">
|
|
200
|
-
<div class="text-base text-gray-400 mb-1">Received</div>
|
|
201
|
-
<div class="text-lg text-gray-300">${new Date(msg.createdAt).toLocaleString()}</div>
|
|
202
|
-
</div>
|
|
203
|
-
</div>
|
|
204
|
-
|
|
205
|
-
<h1 class="text-3xl font-bold text-white mb-6 pt-4 border-t border-gray-800">${msg.subject || '(no subject)'}</h1>
|
|
206
|
-
|
|
207
|
-
${msg.encrypted && msg.decrypted ? `
|
|
208
|
-
<div class="bg-green-900/20 border border-green-900/50 text-green-400 px-3 py-2 rounded-lg text-base inline-flex items-center gap-2 mb-4">
|
|
209
|
-
🔐 Decrypted by: ${msg.decryptedBy || 'unknown'}
|
|
210
|
-
</div>
|
|
211
|
-
` : ''}
|
|
212
|
-
|
|
213
|
-
${msg.encrypted && !msg.decrypted ? `
|
|
214
|
-
<div class="bg-red-900/20 border border-red-900/50 text-red-400 px-3 py-2 rounded-lg text-base inline-flex items-center gap-2 mb-4">
|
|
215
|
-
🔒 Could not decrypt message
|
|
216
|
-
</div>
|
|
217
|
-
` : ''}
|
|
218
|
-
</div>
|
|
219
|
-
|
|
220
|
-
<div class="prose prose-invert max-w-none bg-gray-900 p-8 rounded-xl border border-gray-800 shadow-sm">
|
|
221
|
-
<p class="whitespace-pre-wrap font-sans text-xl text-gray-300 leading-relaxed">${escapeHtml(msg.body)}</p>
|
|
222
|
-
</div>
|
|
223
|
-
|
|
224
|
-
${msg.attachments && msg.attachments.length > 0 ? `
|
|
225
|
-
<!-- Attachments -->
|
|
226
|
-
<div class="mt-6 bg-gray-900 p-6 rounded-xl border border-gray-800">
|
|
227
|
-
<h3 class="text-lg font-semibold text-gray-300 mb-4">📎 Attachments (${msg.attachments.length})</h3>
|
|
228
|
-
<div class="space-y-3">
|
|
229
|
-
${msg.attachments.map((att, i) => `
|
|
230
|
-
<div class="flex items-center justify-between bg-gray-850 p-4 rounded-lg border border-gray-700">
|
|
231
|
-
<div class="flex items-center gap-3">
|
|
232
|
-
<span class="text-2xl">${getFileIcon(att.type)}</span>
|
|
233
|
-
<div>
|
|
234
|
-
<div class="text-white font-medium">${escapeHtml(att.name)}</div>
|
|
235
|
-
<div class="text-sm text-gray-500">${formatFileSize(att.size)}</div>
|
|
236
|
-
</div>
|
|
237
|
-
</div>
|
|
238
|
-
<button onclick='downloadAttachment(${JSON.stringify(att.name)}, ${JSON.stringify(att.type)}, ${JSON.stringify(att.data)})'
|
|
239
|
-
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-500 transition-colors text-lg">
|
|
240
|
-
⬇️ Download
|
|
241
|
-
</button>
|
|
242
|
-
</div>
|
|
243
|
-
`).join('')}
|
|
244
|
-
</div>
|
|
245
|
-
</div>
|
|
246
|
-
` : ''}
|
|
247
|
-
|
|
248
|
-
<!-- Action Buttons -->
|
|
249
|
-
<div class="mt-8 flex gap-4 justify-between">
|
|
250
|
-
<div class="flex gap-3">
|
|
251
|
-
<button onclick="showReplyForm('${msg.id}', '${msg.sender}')"
|
|
252
|
-
class="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-500 transition-colors flex items-center gap-2 font-medium text-lg">
|
|
253
|
-
<span>↩️</span> Reply
|
|
254
|
-
</button>
|
|
255
|
-
<button onclick="forwardMessage('${msg.id}')"
|
|
256
|
-
class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-500 transition-colors flex items-center gap-2 font-medium text-lg">
|
|
257
|
-
<span>➡️</span> Forward
|
|
258
|
-
</button>
|
|
259
|
-
</div>
|
|
260
|
-
<div class="flex gap-3">
|
|
261
|
-
<button onclick="archiveMessage('${msg.id}')"
|
|
262
|
-
class="px-6 py-3 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 hover:text-white transition-colors flex items-center gap-2 font-medium border border-gray-700 text-lg">
|
|
263
|
-
<span>📦</span> Archive
|
|
264
|
-
</button>
|
|
265
|
-
<button onclick="deleteMessage('${msg.id}')"
|
|
266
|
-
class="px-6 py-3 bg-red-900/50 text-red-300 rounded-lg hover:bg-red-800 hover:text-white transition-colors flex items-center gap-2 font-medium border border-red-900/50 text-lg">
|
|
267
|
-
<span>🗑️</span> Delete
|
|
268
|
-
</button>
|
|
269
|
-
</div>
|
|
270
|
-
</div>
|
|
271
|
-
|
|
272
|
-
<!-- Reply Form (hidden by default) -->
|
|
273
|
-
<div id="reply-form" class="hidden mt-8 bg-gray-850 p-6 rounded-xl border border-gray-800">
|
|
274
|
-
<h3 class="text-xl font-bold mb-4">Reply to <span id="reply-to"></span></h3>
|
|
275
|
-
<div class="mb-4">
|
|
276
|
-
<label class="block text-gray-400 mb-2 text-lg">From (your agent ID):</label>
|
|
277
|
-
<input type="text" id="reply-from" value="${currentAgentId}"
|
|
278
|
-
class="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-3 text-lg text-white focus:border-purple-500 focus:outline-none">
|
|
279
|
-
</div>
|
|
280
|
-
<div class="mb-4">
|
|
281
|
-
<label class="block text-gray-400 mb-2 text-lg">Message:</label>
|
|
282
|
-
<textarea id="reply-body" rows="6"
|
|
283
|
-
class="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-3 text-lg text-white focus:border-purple-500 focus:outline-none resize-none"></textarea>
|
|
284
|
-
</div>
|
|
285
|
-
<div class="mb-4">
|
|
286
|
-
<label class="block text-gray-400 mb-2 text-lg">Attach Files (max 5MB each):</label>
|
|
287
|
-
<input type="file" id="reply-files" multiple
|
|
288
|
-
class="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-3 text-lg text-white file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:bg-purple-600 file:text-white file:cursor-pointer">
|
|
289
|
-
<div id="file-list" class="mt-2 text-sm text-gray-400"></div>
|
|
290
|
-
</div>
|
|
291
|
-
<div class="flex gap-4 justify-end">
|
|
292
|
-
<button onclick="hideReplyForm()"
|
|
293
|
-
class="px-6 py-3 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 text-lg">Cancel</button>
|
|
294
|
-
<button onclick="sendReply()"
|
|
295
|
-
class="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-500 font-medium text-lg">Send Reply</button>
|
|
296
|
-
</div>
|
|
297
|
-
</div>
|
|
298
|
-
</div>
|
|
299
|
-
`;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function escapeHtml(text) {
|
|
303
|
-
if (!text) return '';
|
|
304
|
-
const div = document.createElement('div');
|
|
305
|
-
div.textContent = text;
|
|
306
|
-
return div.innerHTML;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function readFileAsBase64(file) {
|
|
310
|
-
return new Promise((resolve, reject) => {
|
|
311
|
-
const reader = new FileReader();
|
|
312
|
-
reader.onload = () => {
|
|
313
|
-
// Remove data URL prefix to get just base64
|
|
314
|
-
const base64 = reader.result.split(',')[1];
|
|
315
|
-
resolve(base64);
|
|
316
|
-
};
|
|
317
|
-
reader.onerror = reject;
|
|
318
|
-
reader.readAsDataURL(file);
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function formatFileSize(bytes) {
|
|
323
|
-
if (bytes < 1024) return bytes + ' B';
|
|
324
|
-
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
325
|
-
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function downloadAttachment(name, type, data) {
|
|
329
|
-
const link = document.createElement('a');
|
|
330
|
-
link.href = `data:${type};base64,${data}`;
|
|
331
|
-
link.download = name;
|
|
332
|
-
link.click();
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function getFileIcon(mimeType) {
|
|
336
|
-
if (!mimeType) return '📄';
|
|
337
|
-
if (mimeType.startsWith('image/')) return '🖼️';
|
|
338
|
-
if (mimeType.startsWith('video/')) return '🎬';
|
|
339
|
-
if (mimeType.startsWith('audio/')) return '🎵';
|
|
340
|
-
if (mimeType.includes('pdf')) return '📕';
|
|
341
|
-
if (mimeType.includes('zip') || mimeType.includes('archive')) return '📦';
|
|
342
|
-
if (mimeType.includes('text')) return '📝';
|
|
343
|
-
if (mimeType.includes('json') || mimeType.includes('javascript')) return '💻';
|
|
344
|
-
return '📄';
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
let replyToRecipient = '';
|
|
348
|
-
let replyToSubject = '';
|
|
349
|
-
|
|
350
|
-
function showReplyForm(msgId, sender) {
|
|
351
|
-
// If replying to a message you sent, the reply should go back to the recipient
|
|
352
|
-
// But we don't have that info easily, so just warn
|
|
353
|
-
if (sender === currentAgentId) {
|
|
354
|
-
const actualRecipient = prompt(`You sent this message. Who should the reply go to?`, sender);
|
|
355
|
-
if (!actualRecipient) return;
|
|
356
|
-
replyToRecipient = actualRecipient;
|
|
357
|
-
} else {
|
|
358
|
-
replyToRecipient = sender;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const currentMsg = document.querySelector('h1').textContent;
|
|
362
|
-
replyToSubject = currentMsg.startsWith('Re:') ? currentMsg : `Re: ${currentMsg}`;
|
|
363
|
-
|
|
364
|
-
document.getElementById('reply-to').textContent = replyToRecipient;
|
|
365
|
-
document.getElementById('reply-form').classList.remove('hidden');
|
|
366
|
-
document.getElementById('reply-body').focus();
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function hideReplyForm() {
|
|
370
|
-
document.getElementById('reply-form').classList.add('hidden');
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
async function sendReply() {
|
|
374
|
-
const from = document.getElementById('reply-from').value;
|
|
375
|
-
const body = document.getElementById('reply-body').value;
|
|
376
|
-
const fileInput = document.getElementById('reply-files');
|
|
377
|
-
|
|
378
|
-
if (!body.trim()) {
|
|
379
|
-
alert('Please enter a message');
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Process attachments
|
|
384
|
-
const attachments = [];
|
|
385
|
-
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
|
|
386
|
-
|
|
387
|
-
if (fileInput && fileInput.files.length > 0) {
|
|
388
|
-
for (const file of fileInput.files) {
|
|
389
|
-
if (file.size > MAX_SIZE) {
|
|
390
|
-
alert(`File "${file.name}" exceeds 5MB limit`);
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const base64 = await readFileAsBase64(file);
|
|
395
|
-
attachments.push({
|
|
396
|
-
name: file.name,
|
|
397
|
-
type: file.type || 'application/octet-stream',
|
|
398
|
-
data: base64,
|
|
399
|
-
size: file.size
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
try {
|
|
405
|
-
console.log('Sending reply to:', replyToRecipient, 'from:', from);
|
|
406
|
-
const response = await fetch('/api/send', {
|
|
407
|
-
method: 'POST',
|
|
408
|
-
headers: { 'Content-Type': 'application/json' },
|
|
409
|
-
body: JSON.stringify({
|
|
410
|
-
to: replyToRecipient,
|
|
411
|
-
subject: replyToSubject,
|
|
412
|
-
body: body,
|
|
413
|
-
from: from,
|
|
414
|
-
attachments: attachments.length > 0 ? attachments : undefined
|
|
415
|
-
})
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
const result = await response.json();
|
|
419
|
-
console.log('Reply result:', result);
|
|
420
|
-
|
|
421
|
-
if (!response.ok || !result.success) {
|
|
422
|
-
throw new Error(result.error || 'Failed to send');
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
hideReplyForm();
|
|
426
|
-
alert('Reply sent!' + (attachments.length > 0 ? ` (${attachments.length} attachment(s))` : ''));
|
|
427
|
-
loadInbox(true);
|
|
428
|
-
} catch (err) {
|
|
429
|
-
console.error('Send reply error:', err);
|
|
430
|
-
alert('Failed to send reply: ' + err.message);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
async function forwardMessage(id) {
|
|
435
|
-
const recipient = prompt('Forward to (agent ID):');
|
|
436
|
-
if (!recipient) return;
|
|
437
|
-
|
|
438
|
-
try {
|
|
439
|
-
const res = await fetch(`/api/message/${id}`);
|
|
440
|
-
const msg = await res.json();
|
|
441
|
-
|
|
442
|
-
const from = prompt('Your agent ID:', currentAgentId);
|
|
443
|
-
if (!from) return;
|
|
444
|
-
|
|
445
|
-
await fetch('/api/send', {
|
|
446
|
-
method: 'POST',
|
|
447
|
-
headers: { 'Content-Type': 'application/json' },
|
|
448
|
-
body: JSON.stringify({
|
|
449
|
-
to: recipient,
|
|
450
|
-
subject: `Fwd: ${msg.subject || '(no subject)'}`,
|
|
451
|
-
body: `---------- Forwarded message ----------\nFrom: ${msg.sender}\nTo: ${msg.recipient}\n\n${msg.body}`,
|
|
452
|
-
from: from
|
|
453
|
-
})
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
alert('Message forwarded!');
|
|
457
|
-
} catch (err) {
|
|
458
|
-
alert('Failed to forward message');
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
async function deleteMessage(id) {
|
|
463
|
-
if (!confirm('Delete this message permanently?')) return;
|
|
464
|
-
|
|
465
|
-
try {
|
|
466
|
-
await fetch(`/api/message/${id}`, { method: 'DELETE' });
|
|
467
|
-
|
|
468
|
-
// Clear detail view
|
|
469
|
-
const detail = document.getElementById('message-detail');
|
|
470
|
-
detail.innerHTML = `
|
|
471
|
-
<div class="flex flex-col items-center justify-center h-full text-gray-500 gap-4">
|
|
472
|
-
<span class="text-6xl opacity-20">🗑️</span>
|
|
473
|
-
<p>Message deleted</p>
|
|
474
|
-
</div>
|
|
475
|
-
`;
|
|
476
|
-
|
|
477
|
-
currentMessageId = null;
|
|
478
|
-
loadInbox(false);
|
|
479
|
-
} catch (err) {
|
|
480
|
-
alert('Failed to delete message');
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
async function archiveMessage(id) {
|
|
485
|
-
if (!confirm('Archive this message?')) return;
|
|
486
|
-
|
|
487
|
-
try {
|
|
488
|
-
await fetch(`/api/message/${id}/archive`, { method: 'POST' });
|
|
489
|
-
|
|
490
|
-
// Clear detail view
|
|
491
|
-
const detail = document.getElementById('message-detail');
|
|
492
|
-
detail.innerHTML = `
|
|
493
|
-
<div class="flex flex-col items-center justify-center h-full text-gray-500 gap-4">
|
|
494
|
-
<span class="text-6xl opacity-20">📦</span>
|
|
495
|
-
<p>Message archived</p>
|
|
496
|
-
</div>
|
|
497
|
-
`;
|
|
498
|
-
|
|
499
|
-
currentMessageId = null;
|
|
500
|
-
loadInbox(false);
|
|
501
|
-
} catch (err) {
|
|
502
|
-
alert('Failed to archive message');
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function updateStats(data) {
|
|
507
|
-
const unreadCount = data.messages.filter(m => !m.read && (!m.readBy || !m.readBy.includes(currentAgentId))).length;
|
|
508
|
-
document.getElementById('stats').innerHTML = `
|
|
509
|
-
<span class="mr-3">Total: <span class="text-white font-bold">${data.total}</span></span>
|
|
510
|
-
<span class="${unreadCount > 0 ? 'text-blue-400 font-bold' : ''}">Unread: ${unreadCount}</span>
|
|
511
|
-
`;
|
|
512
|
-
|
|
513
|
-
// Update tab title
|
|
514
|
-
document.title = unreadCount > 0 ? `(${unreadCount}) Myceliumail` : 'Myceliumail Dashboard';
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Initial load
|
|
518
|
-
requestNotificationPermission();
|
|
519
|
-
setupRealtime();
|
|
520
|
-
loadInbox();
|
|
521
|
-
|
|
522
|
-
// Poll every 30 seconds as fallback (Realtime handles instant updates)
|
|
523
|
-
setInterval(() => loadInbox(true), 30000);
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
|
|
4
|
-
<head>
|
|
5
|
-
<meta charset="UTF-8">
|
|
6
|
-
<title>🍄 Myceliumail Dashboard</title>
|
|
7
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
-
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
|
|
9
|
-
<link rel="stylesheet" href="/styles.css">
|
|
10
|
-
<script>
|
|
11
|
-
tailwind.config = {
|
|
12
|
-
theme: {
|
|
13
|
-
extend: {
|
|
14
|
-
colors: {
|
|
15
|
-
gray: {
|
|
16
|
-
850: '#1f2937',
|
|
17
|
-
950: '#030712',
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
</script>
|
|
24
|
-
</head>
|
|
25
|
-
|
|
26
|
-
<body class="bg-gray-950 text-gray-100 min-h-screen">
|
|
27
|
-
<div class="container mx-auto p-6 h-screen flex flex-col">
|
|
28
|
-
<!-- Header -->
|
|
29
|
-
<header class="flex justify-between items-center mb-6 shrink-0">
|
|
30
|
-
<div class="flex items-center gap-3">
|
|
31
|
-
<span class="text-4xl">🍄</span>
|
|
32
|
-
<h1
|
|
33
|
-
class="text-3xl font-bold bg-gradient-to-r from-purple-400 to-pink-600 bg-clip-text text-transparent">
|
|
34
|
-
Myceliumail</h1>
|
|
35
|
-
</div>
|
|
36
|
-
<div id="stats" class="text-sm text-gray-400 bg-gray-900 px-4 py-2 rounded-full border border-gray-800">
|
|
37
|
-
Loading...
|
|
38
|
-
</div>
|
|
39
|
-
</header>
|
|
40
|
-
|
|
41
|
-
<!-- Main Content -->
|
|
42
|
-
<div class="grid grid-cols-12 gap-6 flex-1 min-h-0">
|
|
43
|
-
<!-- Inbox List -->
|
|
44
|
-
<div class="col-span-4 bg-gray-900 rounded-xl border border-gray-800 flex flex-col overflow-hidden">
|
|
45
|
-
<div
|
|
46
|
-
class="p-4 border-b border-gray-800 bg-gray-900/50 backdrop-blur flex items-center justify-between">
|
|
47
|
-
<h2 class="text-xl font-semibold text-gray-200">Inbox</h2>
|
|
48
|
-
<button onclick="loadInbox(false)"
|
|
49
|
-
class="p-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-lg transition-colors"
|
|
50
|
-
title="Refresh inbox">
|
|
51
|
-
🔄
|
|
52
|
-
</button>
|
|
53
|
-
</div>
|
|
54
|
-
<div id="inbox-list" class="flex-1 overflow-y-auto p-2 space-y-2">
|
|
55
|
-
<!-- Messages will act here -->
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
<!-- Message Detail -->
|
|
60
|
-
<div
|
|
61
|
-
class="col-span-8 bg-gray-900 rounded-xl border border-gray-800 flex flex-col overflow-hidden relative">
|
|
62
|
-
<div id="message-detail" class="flex-1 overflow-y-auto p-8">
|
|
63
|
-
<div class="flex flex-col items-center justify-center h-full text-gray-500 gap-4">
|
|
64
|
-
<span class="text-6xl opacity-20">📨</span>
|
|
65
|
-
<p>Select a message to view</p>
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
<script src="/app.js"></script>
|
|
73
|
-
</body>
|
|
74
|
-
|
|
75
|
-
</html>
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
html {
|
|
2
|
-
font-size: clamp(18px, 2.5vw, 22px);
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
body {
|
|
6
|
-
scrollbar-gutter: stable;
|
|
7
|
-
font-size: 1.1rem;
|
|
8
|
-
line-height: 1.6;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
::-webkit-scrollbar {
|
|
12
|
-
width: 10px;
|
|
13
|
-
height: 10px;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
::-webkit-scrollbar-track {
|
|
17
|
-
background: #111827;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
::-webkit-scrollbar-thumb {
|
|
21
|
-
background: #374151;
|
|
22
|
-
border-radius: 5px;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
::-webkit-scrollbar-thumb:hover {
|
|
26
|
-
background: #4b5563;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.anim-fade-in {
|
|
30
|
-
animation: fadeIn 0.3s ease-out;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
@keyframes fadeIn {
|
|
34
|
-
from {
|
|
35
|
-
opacity: 0;
|
|
36
|
-
transform: translateY(10px);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
to {
|
|
40
|
-
opacity: 1;
|
|
41
|
-
transform: translateY(0);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.message-item:active {
|
|
46
|
-
transform: scale(0.98);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/* Larger text helpers */
|
|
50
|
-
.text-base {
|
|
51
|
-
font-size: 1.1rem !important;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.text-lg {
|
|
55
|
-
font-size: 1.25rem !important;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
.text-xl {
|
|
59
|
-
font-size: 1.5rem !important;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.text-2xl {
|
|
63
|
-
font-size: 1.75rem !important;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.text-3xl {
|
|
67
|
-
font-size: 2rem !important;
|
|
68
|
-
}
|