← back to Slack Dm Viewer
initial scaffold (gitify-all 2026-05-06)
fe31fd7ad47c90152e14f1499957088fd42e0771 · 2026-05-06 10:25:48 -0700 · Steve Abrams
Files touched
A .gitignoreA claude-steve.htmlA ecosystem.config.jsA package-lock.jsonA package.jsonA server.js
Diff
commit fe31fd7ad47c90152e14f1499957088fd42e0771
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 6 10:25:48 2026 -0700
initial scaffold (gitify-all 2026-05-06)
---
.gitignore | 12 +
claude-steve.html | 361 +++++++++++++++
ecosystem.config.js | 20 +
package-lock.json | 837 ++++++++++++++++++++++++++++++++++
package.json | 14 +
server.js | 1251 +++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 2495 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7e6a9c3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+node_modules/
+.env
+.env.local
+.env.*.local
+.env.*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+*.bak
diff --git a/claude-steve.html b/claude-steve.html
new file mode 100644
index 0000000..0b386de
--- /dev/null
+++ b/claude-steve.html
@@ -0,0 +1,361 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Claude + Steve | Message Viewer</title>
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
+ <style>
+ :root {
+ --bg-primary: #0a0a0f;
+ --bg-secondary: #111118;
+ --bg-card: #16161f;
+ --bg-hover: #1c1c28;
+ --border: #252535;
+ --border-accent: #3b3b55;
+ --text-primary: #e8e8f0;
+ --text-secondary: #8888a0;
+ --text-muted: #555568;
+ --accent: #6c5ce7;
+ --accent-light: #a29bfe;
+ --accent-glow: rgba(108,92,231,0.15);
+ --green: #00b894;
+ --red: #e17055;
+ --link: #74b9ff;
+ --radius: 12px;
+ --radius-sm: 8px;
+ --radius-xs: 4px;
+ --shadow: 0 4px 24px rgba(0,0,0,0.3);
+ }
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+ body {
+ background: var(--bg-primary); color: var(--text-primary);
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
+ min-height: 100vh; overflow-x: hidden;
+ }
+ body::before {
+ content: ''; position: fixed; top: -200px; left: 50%; transform: translateX(-50%);
+ width: 800px; height: 600px;
+ background: radial-gradient(ellipse, rgba(108,92,231,0.06) 0%, transparent 70%);
+ pointer-events: none; z-index: 0;
+ }
+ .topnav {
+ position: sticky; top: 0; z-index: 100;
+ background: rgba(10,10,15,0.85); backdrop-filter: blur(20px);
+ border-bottom: 1px solid var(--border); padding: 0 24px; height: 56px;
+ display: flex; align-items: center; justify-content: space-between;
+ }
+ .topnav-left { display: flex; align-items: center; gap: 12px; }
+ .topnav-logo {
+ width: 32px; height: 32px; border-radius: 8px;
+ background: linear-gradient(135deg, var(--accent), #8b5cf6);
+ display: flex; align-items: center; justify-content: center;
+ font-size: 16px; font-weight: 700; color: #fff;
+ box-shadow: 0 0 20px rgba(108,92,231,0.3);
+ }
+ .topnav-title { font-size: 15px; font-weight: 600; letter-spacing: -0.3px; }
+ .topnav-subtitle { font-size: 11px; color: var(--text-muted); margin-top: 1px; }
+ .topnav-right { display: flex; align-items: center; gap: 10px; }
+ .badge { padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 500; letter-spacing: 0.3px; }
+ .badge-live { background: rgba(0,184,148,0.15); color: var(--green); }
+ .badge-count { background: var(--accent-glow); color: var(--accent-light); }
+ .btn {
+ padding: 7px 14px; border-radius: var(--radius-sm); border: 1px solid var(--border);
+ background: var(--bg-card); color: var(--text-primary); font-size: 12px;
+ font-weight: 500; cursor: pointer; transition: all 0.2s; font-family: inherit;
+ }
+ .btn:hover { background: var(--bg-hover); border-color: var(--border-accent); }
+ .btn:disabled { opacity: 0.4; cursor: not-allowed; }
+ .search-bar {
+ background: var(--bg-secondary); border-bottom: 1px solid var(--border);
+ padding: 10px 24px; display: flex; align-items: center; gap: 10px;
+ }
+ .search-wrap { flex: 1; position: relative; }
+ .search-wrap .s-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--text-muted); font-size: 14px; }
+ .search-wrap input {
+ width: 100%; padding: 9px 12px 9px 36px; background: var(--bg-card);
+ border: 1px solid var(--border); border-radius: var(--radius-sm);
+ color: var(--text-primary); font-size: 13px; font-family: inherit; outline: none;
+ }
+ .search-wrap input:focus { border-color: var(--accent); }
+ .search-wrap input::placeholder { color: var(--text-muted); }
+ .filter-chip {
+ padding: 5px 12px; border-radius: 20px; border: 1px solid var(--border);
+ background: transparent; color: var(--text-secondary); font-size: 12px;
+ cursor: pointer; transition: all 0.2s; font-family: inherit; white-space: nowrap;
+ }
+ .filter-chip:hover, .filter-chip.active { background: var(--accent-glow); border-color: var(--accent); color: var(--accent-light); }
+ .main { max-width: 820px; margin: 0 auto; padding: 16px 20px 80px; position: relative; z-index: 1; }
+ .date-divider { display: flex; align-items: center; gap: 16px; margin: 24px 0 12px; }
+ .date-divider::before, .date-divider::after { content: ''; flex: 1; height: 1px; background: linear-gradient(90deg, transparent, var(--border), transparent); }
+ .date-label { font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; white-space: nowrap; }
+ .msg { display: flex; gap: 12px; padding: 12px 16px; border-radius: var(--radius); transition: background 0.15s; margin-bottom: 2px; }
+ .msg:hover { background: var(--bg-hover); }
+ .msg.search-hit { background: var(--accent-glow); border-left: 3px solid var(--accent); }
+ .msg-avatar {
+ width: 36px; height: 36px; border-radius: 10px;
+ background: linear-gradient(135deg, #2563eb, #1e40af);
+ display: flex; align-items: center; justify-content: center;
+ font-size: 13px; font-weight: 700; color: #fff; flex-shrink: 0; margin-top: 2px;
+ }
+ .msg-body { flex: 1; min-width: 0; }
+ .msg-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 3px; }
+ .msg-name { font-size: 14px; font-weight: 600; }
+ .msg-time { font-size: 11px; color: var(--text-muted); }
+ .msg-text { font-size: 14px; line-height: 1.6; color: var(--text-secondary); word-break: break-word; }
+ .msg-text a { color: var(--link); text-decoration: none; }
+ .msg-text a:hover { text-decoration: underline; }
+ .msg-text code { background: var(--bg-card); border: 1px solid var(--border); padding: 1px 5px; border-radius: var(--radius-xs); font-family: monospace; font-size: 12px; color: var(--accent-light); }
+ .msg-text pre { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 12px 16px; margin: 8px 0; overflow-x: auto; font-family: monospace; font-size: 12px; line-height: 1.5; }
+ .msg-text blockquote { border-left: 3px solid var(--accent); padding-left: 12px; margin: 6px 0; color: var(--text-muted); font-style: italic; }
+ .att { margin-top: 8px; padding: 10px 14px; background: var(--bg-card); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: var(--radius-sm); }
+ .att-title { font-size: 13px; font-weight: 600; color: var(--link); margin-bottom: 4px; }
+ .att-title a { color: var(--link); text-decoration: none; }
+ .att-title a:hover { text-decoration: underline; }
+ .att-desc { font-size: 12px; color: var(--text-muted); line-height: 1.5; }
+ .att img { max-width: 400px; max-height: 280px; border-radius: var(--radius-sm); margin-top: 8px; border: 1px solid var(--border); }
+ .loading-wrap { text-align: center; padding: 60px 20px; }
+ .loading-spinner { width: 36px; height: 36px; border: 3px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto 14px; }
+ @keyframes spin { to { transform: rotate(360deg); } }
+ .loading-text { font-size: 13px; color: var(--text-muted); }
+ .empty { text-align: center; padding: 80px 20px; color: var(--text-muted); }
+ .empty-icon { font-size: 40px; margin-bottom: 12px; opacity: 0.5; }
+ .empty h3 { font-size: 16px; color: var(--text-secondary); margin-bottom: 6px; }
+ .stats-row { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; }
+ .stat-card { flex: 1; min-width: 140px; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; }
+ .stat-card .label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
+ .stat-card .value { font-size: 22px; font-weight: 700; }
+ .stat-card .sub { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
+ .toast { position: fixed; bottom: 24px; right: 24px; padding: 10px 18px; border-radius: var(--radius-sm); font-size: 13px; z-index: 1000; animation: slideIn 0.3s ease; max-width: 360px; box-shadow: var(--shadow); }
+ .toast-ok { background: rgba(0,184,148,0.15); color: var(--green); border: 1px solid rgba(0,184,148,0.3); }
+ .toast-err { background: rgba(225,112,85,0.15); color: var(--red); border: 1px solid rgba(225,112,85,0.3); }
+ @keyframes slideIn { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
+ .back-link { color: var(--text-muted); text-decoration: none; font-size: 12px; }
+ .back-link:hover { color: var(--text-primary); }
+ @media (max-width: 640px) {
+ .topnav { padding: 0 14px; } .search-bar { padding: 8px 14px; }
+ .main { padding: 10px 12px 60px; } .msg { padding: 10px 8px; }
+ .msg-avatar { width: 28px; height: 28px; font-size: 11px; }
+ .stat-card { min-width: 100px; } .att img { max-width: 100%; }
+ }
+ </style>
+</head>
+<body>
+ <div id="app"></div>
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js" crossorigin></script>
+ <script src="https://unpkg.com/dompurify@3.0.6/dist/purify.min.js" crossorigin></script>
+ <script type="text/babel">
+ const { useState, useEffect, useMemo, useCallback } = React;
+ const AUTH = 'Basic ' + btoa('admin:I3YusisdESUNdxtrmlb3QJeu9q8ODKJO');
+
+ function fmtTime(ts) {
+ if (!ts) return '';
+ const d = new Date(parseFloat(ts) * 1000);
+ return isNaN(d.getTime()) ? '' : d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true, timeZone: 'America/Los_Angeles' });
+ }
+ function fmtDate(ts) {
+ if (!ts) return '';
+ const d = new Date(parseFloat(ts) * 1000);
+ if (isNaN(d.getTime())) return '';
+ const now = new Date();
+ const y = new Date(now); y.setDate(y.getDate() - 1);
+ if (d.toDateString() === now.toDateString()) return 'Today';
+ if (d.toDateString() === y.toDateString()) return 'Yesterday';
+ return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', timeZone: 'America/Los_Angeles' });
+ }
+ function dateKey(ts) { return ts ? new Date(parseFloat(ts) * 1000).toDateString() : '-'; }
+
+ function renderText(raw) {
+ if (!raw) return '';
+ let html = raw
+ .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
+ .replace(/<(https?:\/\/[^|>]+)\|([^>]+)>/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$2</a>')
+ .replace(/<(https?:\/\/[^>]+)>/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>')
+ .replace(/```([\s\S]*?)```/g, '<pre>$1</pre>')
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
+ .replace(/\*([^*]+)\*/g, '<strong>$1</strong>')
+ .replace(/_([^_]+)_/g, '<em>$1</em>')
+ .replace(/~([^~]+)~/g, '<del>$1</del>')
+ .replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
+ .replace(/\n/g, '<br>');
+ return DOMPurify.sanitize(html, { ALLOWED_TAGS: ['a','strong','em','del','code','pre','blockquote','br'], ALLOWED_ATTR: ['href','target','rel'] });
+ }
+
+ function Toast({ msg, type, onClose }) {
+ useEffect(() => { const t = setTimeout(onClose, 4000); return () => clearTimeout(t); }, [onClose]);
+ return React.createElement('div', { className: 'toast toast-' + type }, msg);
+ }
+
+ function Msg({ m, search }) {
+ const isHit = search && m.text && m.text.toLowerCase().includes(search.toLowerCase());
+ const sanitized = renderText(m.text);
+ const textEl = React.createElement('div', { className: 'msg-text', dangerouslySetInnerHTML: { __html: sanitized } });
+
+ const atts = (m.attachments || []).map((a, ai) =>
+ React.createElement('div', { key: ai, className: 'att' },
+ a.title && React.createElement('div', { className: 'att-title' },
+ a.title_link ? React.createElement('a', { href: DOMPurify.sanitize(a.title_link), target: '_blank', rel: 'noopener noreferrer' }, a.title) : a.title),
+ a.text && React.createElement('div', { className: 'att-desc' }, a.text),
+ !a.title && !a.text && a.fallback && React.createElement('div', { className: 'att-desc' }, a.fallback),
+ a.image_url && React.createElement('img', { src: DOMPurify.sanitize(a.image_url), alt: '', loading: 'lazy' }),
+ a.thumb_url && !a.image_url && React.createElement('img', { src: DOMPurify.sanitize(a.thumb_url), alt: '', loading: 'lazy' })
+ )
+ );
+
+ const files = (m.files || []).map((f, fi) =>
+ React.createElement('div', { key: fi, className: 'att' },
+ React.createElement('div', { className: 'att-title' },
+ f.url_private ? React.createElement('a', { href: DOMPurify.sanitize(f.url_private), target: '_blank', rel: 'noopener noreferrer' }, f.name || 'File') : (f.name || 'File')),
+ f.mimetype && f.mimetype.startsWith('image/') && f.url_private && React.createElement('img', { src: DOMPurify.sanitize(f.url_private), alt: f.name || '', loading: 'lazy' })
+ )
+ );
+
+ return React.createElement('div', { className: 'msg' + (isHit ? ' search-hit' : '') },
+ React.createElement('div', { className: 'msg-avatar' }, 'SA'),
+ React.createElement('div', { className: 'msg-body' },
+ React.createElement('div', { className: 'msg-head' },
+ React.createElement('span', { className: 'msg-name' }, 'Steve Abrams'),
+ React.createElement('span', { className: 'msg-time' }, fmtTime(m.ts))
+ ),
+ textEl,
+ ...atts,
+ ...files
+ )
+ );
+ }
+
+ function App() {
+ const [messages, setMessages] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [search, setSearch] = useState('');
+ const [filter, setFilter] = useState('all');
+ const [toast, setToast] = useState(null);
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ try {
+ const r = await fetch('/api/claude-steve/messages', { headers: { Authorization: AUTH } });
+ const data = await r.json();
+ if (data.ok) {
+ setMessages(data.messages || []);
+ setToast({ msg: data.count + ' messages loaded', type: 'ok' });
+ } else {
+ setToast({ msg: data.error || 'Failed', type: 'err' });
+ }
+ } catch (e) { setToast({ msg: e.message, type: 'err' }); }
+ finally { setLoading(false); }
+ }, []);
+
+ useEffect(() => { load(); }, [load]);
+
+ const filtered = useMemo(() => {
+ let msgs = messages;
+ if (filter === 'links') msgs = msgs.filter(m => /https?:\/\//.test(m.text || ''));
+ else if (filter === 'media') msgs = msgs.filter(m => (m.attachments && m.attachments.length) || (m.files && m.files.length));
+ if (search.trim()) { const q = search.toLowerCase(); msgs = msgs.filter(m => (m.text || '').toLowerCase().includes(q)); }
+ return msgs;
+ }, [messages, search, filter]);
+
+ const grouped = useMemo(() => {
+ const g = {};
+ filtered.forEach(m => { const k = dateKey(m.ts); if (!g[k]) g[k] = { ts: m.ts, msgs: [] }; g[k].msgs.push(m); });
+ return Object.values(g);
+ }, [filtered]);
+
+ const stats = useMemo(() => {
+ if (!messages.length) return { total: 0, days: 0, links: 0, oldest: '' };
+ const links = messages.filter(m => /https?:\/\//.test(m.text || '')).length;
+ const dates = new Set(messages.map(m => dateKey(m.ts)));
+ const oldest = messages[messages.length - 1];
+ const od = oldest ? new Date(parseFloat(oldest.ts) * 1000) : null;
+ return { total: messages.length, days: dates.size, links,
+ oldest: od ? od.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'America/Los_Angeles' }) : '?' };
+ }, [messages]);
+
+ return React.createElement('div', null,
+ // TopNav
+ React.createElement('div', { className: 'topnav' },
+ React.createElement('div', { className: 'topnav-left' },
+ React.createElement('a', { href: '/', className: 'back-link' }, '\u2190 DMs'),
+ React.createElement('div', { className: 'topnav-logo' }, 'CS'),
+ React.createElement('div', null,
+ React.createElement('div', { className: 'topnav-title' }, 'Claude + Steve'),
+ React.createElement('div', { className: 'topnav-subtitle' }, '#claude-to-steve')
+ )
+ ),
+ React.createElement('div', { className: 'topnav-right' },
+ React.createElement('span', { className: 'badge badge-live' }, 'LIVE'),
+ React.createElement('span', { className: 'badge badge-count' }, filtered.length + ' msgs'),
+ React.createElement('button', { className: 'btn', onClick: load, disabled: loading }, loading ? 'Loading...' : 'Refresh')
+ )
+ ),
+ // Search
+ React.createElement('div', { className: 'search-bar' },
+ React.createElement('div', { className: 'search-wrap' },
+ React.createElement('span', { className: 's-icon' }, '\uD83D\uDD0D'),
+ React.createElement('input', { placeholder: 'Search messages...', value: search, onChange: e => setSearch(e.target.value) })
+ ),
+ React.createElement('button', { className: 'filter-chip' + (filter === 'all' ? ' active' : ''), onClick: () => setFilter('all') }, 'All'),
+ React.createElement('button', { className: 'filter-chip' + (filter === 'links' ? ' active' : ''), onClick: () => setFilter('links') }, 'Links'),
+ React.createElement('button', { className: 'filter-chip' + (filter === 'media' ? ' active' : ''), onClick: () => setFilter('media') }, 'Media')
+ ),
+ // Main
+ React.createElement('div', { className: 'main' },
+ // Stats
+ !loading && messages.length > 0 && React.createElement('div', { className: 'stats-row' },
+ React.createElement('div', { className: 'stat-card' },
+ React.createElement('div', { className: 'label' }, 'Messages'),
+ React.createElement('div', { className: 'value' }, stats.total),
+ React.createElement('div', { className: 'sub' }, "Steve's notes")
+ ),
+ React.createElement('div', { className: 'stat-card' },
+ React.createElement('div', { className: 'label' }, 'Active Days'),
+ React.createElement('div', { className: 'value' }, stats.days),
+ React.createElement('div', { className: 'sub' }, 'Since ' + stats.oldest)
+ ),
+ React.createElement('div', { className: 'stat-card' },
+ React.createElement('div', { className: 'label' }, 'Links Shared'),
+ React.createElement('div', { className: 'value' }, stats.links),
+ React.createElement('div', { className: 'sub' }, Math.round(stats.links / Math.max(stats.total, 1) * 100) + '% of messages')
+ )
+ ),
+ // Loading
+ loading && React.createElement('div', { className: 'loading-wrap' },
+ React.createElement('div', { className: 'loading-spinner' }),
+ React.createElement('div', { className: 'loading-text' }, 'Fetching from #claude-to-steve...')
+ ),
+ // Empty states
+ !loading && filtered.length === 0 && messages.length > 0 && React.createElement('div', { className: 'empty' },
+ React.createElement('div', { className: 'empty-icon' }, '\uD83D\uDD0D'),
+ React.createElement('h3', null, 'No matches')
+ ),
+ !loading && messages.length === 0 && React.createElement('div', { className: 'empty' },
+ React.createElement('div', { className: 'empty-icon' }, '\uD83D\uDCAC'),
+ React.createElement('h3', null, 'No Messages')
+ ),
+ // Messages
+ !loading && filtered.length > 0 && grouped.map((group, gi) =>
+ React.createElement('div', { key: gi },
+ React.createElement('div', { className: 'date-divider' },
+ React.createElement('span', { className: 'date-label' }, fmtDate(group.ts))
+ ),
+ group.msgs.map((m, mi) => React.createElement(Msg, { key: m.ts || mi, m: m, search: search }))
+ )
+ )
+ ),
+ // Toast
+ toast && React.createElement(Toast, { msg: toast.msg, type: toast.type, onClose: () => setToast(null) })
+ );
+ }
+
+ ReactDOM.createRoot(document.getElementById('app')).render(React.createElement(App));
+ </script>
+ <script>
+ window.claudettePageContext = function() { return { dashboard: 'claude-steve-viewer' }; };
+ </script>
+ <script src="http://45.61.58.125:9872/widget.js" data-claudette-endpoint="http://45.61.58.125:9872" defer></script>
+</body>
+</html>
diff --git a/ecosystem.config.js b/ecosystem.config.js
new file mode 100644
index 0000000..7691177
--- /dev/null
+++ b/ecosystem.config.js
@@ -0,0 +1,20 @@
+module.exports = {
+ apps: [{
+ name: 'slack-dm-viewer',
+ script: 'server.js',
+ cwd: '/root/DW-Agents/slack-dm-viewer',
+ env: {
+ NODE_ENV: 'production',
+ PORT: 7204
+ },
+ instances: 1,
+ exec_mode: 'fork',
+ max_memory_restart: '128M',
+ autorestart: true,
+ watch: false,
+ log_date_format: 'YYYY-MM-DD HH:mm:ss',
+ error_file: '/root/DW-Agents/slack-dm-viewer/error.log',
+ out_file: '/root/DW-Agents/slack-dm-viewer/out.log',
+ merge_logs: true
+ }]
+};
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..b6f8b9e
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,837 @@
+{
+ "name": "slack-dm-viewer",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "slack-dm-viewer",
+ "version": "1.0.0",
+ "dependencies": {
+ "express": "^4.18.2",
+ "helmet": "^8.1.0"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/helmet": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
+ "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..956ce29
--- /dev/null
+++ b/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "slack-dm-viewer",
+ "version": "1.0.0",
+ "description": "View Steve's Slack self-DMs / notes to self",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "dev": "node server.js"
+ },
+ "dependencies": {
+ "express": "^4.18.2",
+ "helmet": "^8.1.0"
+ }
+}
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..8cd0ca4
--- /dev/null
+++ b/server.js
@@ -0,0 +1,1251 @@
+const express = require('express');
+const helmet = require('helmet');
+const https = require('https');
+const path = require('path');
+const fs = require('fs');
+
+const app = express();
+// Security headers via helmet (added 2026-05-04 overnight YOLO loop)
+app.use(helmet({ contentSecurityPolicy: false }));
+const PORT = process.env.PORT || 7204;
+
+// --- Auth ---
+const AUTH_USER = 'admin';
+const AUTH_PASS = 'I3YusisdESUNdxtrmlb3QJeu9q8ODKJO';
+
+function basicAuth(req, res, next) {
+ // Skip auth for health endpoint
+ if (req.path === '/health') return next();
+
+ const auth = req.headers.authorization;
+ if (!auth || !auth.startsWith('Basic ')) {
+ res.setHeader('WWW-Authenticate', 'Basic realm="Slacky"');
+ return res.status(401).send('Authentication required');
+ }
+ const decoded = Buffer.from(auth.split(' ')[1], 'base64').toString();
+ const [user, pass] = decoded.split(':');
+ if (user === AUTH_USER && pass === AUTH_PASS) return next();
+ res.setHeader('WWW-Authenticate', 'Basic realm="Slacky"');
+ return res.status(401).send('Invalid credentials');
+}
+
+app.use(express.json({ limit: '50mb' }));
+app.use(basicAuth);
+
+// --- Slack API config ---
+// designerbot token has im:history scope
+const SLACK_BOT_TOKEN = 'xoxb-3958182050256-9852565284259-8kfXhJQWVsf515doMRI67AHQ';
+// dwmcp_bot token has users:read scope
+const SLACK_USERS_TOKEN = 'xoxb-3958182050256-9814366234406-HiR1ybK6HT8JViWDEpjXpmsq';
+
+const STEVE_USER_ID = 'U03TV3UC2V7';
+// Steve's user token for private channel access (#claude-to-steve)
+const SLACK_USER_TOKEN = 'xoxp-3958182050256-3947130410993-10509093793987-db03ca5d0c121d415a816178f4aac1a7';
+const CLAUDE_STEVE_CHANNEL = 'C09SW7VQ0RK';
+
+// Persistent config
+const CONFIG_FILE = path.join(__dirname, 'config.json');
+function loadConfig() {
+ try {
+ if (fs.existsSync(CONFIG_FILE)) return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
+ } catch (e) {}
+ return { channelId: null, cachedMessages: [] };
+}
+function saveConfig(cfg) {
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
+}
+
+// --- Slack API helpers ---
+function slackApi(method, params, token) {
+ return new Promise((resolve, reject) => {
+ const qs = new URLSearchParams(params).toString();
+ const reqPath = `/api/${method}${qs ? '?' + qs : ''}`;
+ const options = {
+ hostname: 'slack.com',
+ path: reqPath,
+ method: 'GET',
+ headers: {
+ 'Authorization': `Bearer ${token || SLACK_BOT_TOKEN}`,
+ 'Content-Type': 'application/json'
+ }
+ };
+ const req = https.request(options, res => {
+ let data = '';
+ res.on('data', chunk => data += chunk);
+ res.on('end', () => {
+ try { resolve(JSON.parse(data)); }
+ catch (e) { reject(new Error('Invalid JSON from Slack')); }
+ });
+ });
+ req.on('error', reject);
+ req.setTimeout(15000, () => { req.destroy(); reject(new Error('Slack API timeout')); });
+ req.end();
+ });
+}
+
+function slackApiPost(method, body, token) {
+ return new Promise((resolve, reject) => {
+ const postData = JSON.stringify(body);
+ const options = {
+ hostname: 'slack.com',
+ path: `/api/${method}`,
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token || SLACK_BOT_TOKEN}`,
+ 'Content-Type': 'application/json; charset=utf-8',
+ 'Content-Length': Buffer.byteLength(postData)
+ }
+ };
+ const req = https.request(options, res => {
+ let data = '';
+ res.on('data', chunk => data += chunk);
+ res.on('end', () => {
+ try { resolve(JSON.parse(data)); }
+ catch (e) { reject(new Error('Invalid JSON from Slack')); }
+ });
+ });
+ req.on('error', reject);
+ req.setTimeout(15000, () => { req.destroy(); reject(new Error('Slack API timeout')); });
+ req.write(postData);
+ req.end();
+ });
+}
+
+// --- API Endpoints ---
+
+// Health check
+app.get('/health', (req, res) => {
+ res.json({ status: 'healthy', service: 'slacky', port: PORT, uptime: process.uptime() });
+});
+
+// Get config
+app.get('/api/config', (req, res) => {
+ const cfg = loadConfig();
+ res.json({ channelId: cfg.channelId, hasCachedMessages: (cfg.cachedMessages || []).length > 0 });
+});
+
+// Set channel ID
+app.post('/api/config/channel', (req, res) => {
+ const { channelId } = req.body;
+ if (!channelId) return res.status(400).json({ error: 'channelId required' });
+ const cfg = loadConfig();
+ cfg.channelId = channelId;
+ saveConfig(cfg);
+ res.json({ ok: true, channelId });
+});
+
+// Auto-detect Steve's self-DM channel
+app.post('/api/detect-channel', async (req, res) => {
+ try {
+ // Try conversations.list with im type using the users token
+ const result = await slackApi('conversations.list', { types: 'im', limit: '200' }, SLACK_USERS_TOKEN);
+ if (result.ok && result.channels) {
+ const selfDm = result.channels.find(c => c.user === STEVE_USER_ID);
+ if (selfDm) {
+ const cfg = loadConfig();
+ cfg.channelId = selfDm.id;
+ saveConfig(cfg);
+ return res.json({ ok: true, channelId: selfDm.id, method: 'auto-detect' });
+ }
+ // If no self-DM, return all DM channels for manual selection
+ return res.json({ ok: false, error: 'Self-DM not found', channels: result.channels });
+ }
+ // If missing_scope, try conversations.open as fallback
+ if (result.error === 'missing_scope') {
+ return res.json({
+ ok: false,
+ error: 'Bot token lacks im:read scope. Please enter your self-DM channel ID manually.',
+ hint: 'In Slack, right-click on your self-DM chat, Copy Link. The channel ID is the last part of the URL (starts with D).'
+ });
+ }
+ res.json({ ok: false, error: result.error || 'Unknown error' });
+ } catch (e) {
+ res.status(500).json({ ok: false, error: e.message });
+ }
+});
+
+// Fetch messages from Slack
+app.get('/api/messages', async (req, res) => {
+ const cfg = loadConfig();
+ const channelId = req.query.channel || cfg.channelId;
+ if (!channelId) {
+ // Return cached messages if available
+ if (cfg.cachedMessages && cfg.cachedMessages.length > 0) {
+ return res.json({ ok: true, messages: cfg.cachedMessages, source: 'cache' });
+ }
+ return res.status(400).json({ ok: false, error: 'No channel ID configured. Set it via /api/config/channel or use the setup page.' });
+ }
+
+ try {
+ let allMessages = [];
+ let cursor = undefined;
+ let pages = 0;
+ const maxPages = 20; // Limit to prevent runaway
+
+ do {
+ const params = { channel: channelId, limit: '200' };
+ if (cursor) params.cursor = cursor;
+
+ const result = await slackApi('conversations.history', params, SLACK_BOT_TOKEN);
+
+ if (!result.ok) {
+ // If we get not_in_channel, try joining first
+ if (result.error === 'not_in_channel' || result.error === 'channel_not_found') {
+ return res.status(400).json({
+ ok: false,
+ error: `Cannot access channel ${channelId}: ${result.error}. The bot must be added to this DM channel. Try the other token or upload JSON export.`,
+ slackError: result.error
+ });
+ }
+ return res.status(400).json({ ok: false, error: result.error });
+ }
+
+ // Filter to only Steve's messages (self-DMs from Steve to Steve)
+ const steveMessages = result.messages.filter(m =>
+ m.user === STEVE_USER_ID && m.subtype !== 'channel_join' && m.subtype !== 'channel_leave'
+ );
+ allMessages = allMessages.concat(steveMessages);
+
+ cursor = result.response_metadata && result.response_metadata.next_cursor;
+ pages++;
+ } while (cursor && pages < maxPages);
+
+ // Cache messages
+ cfg.cachedMessages = allMessages;
+ cfg.channelId = channelId;
+ cfg.lastFetch = new Date().toISOString();
+ saveConfig(cfg);
+
+ res.json({ ok: true, messages: allMessages, count: allMessages.length, source: 'slack-api', pages });
+ } catch (e) {
+ // Return cached on error
+ if (cfg.cachedMessages && cfg.cachedMessages.length > 0) {
+ return res.json({ ok: true, messages: cfg.cachedMessages, source: 'cache-fallback', error: e.message });
+ }
+ res.status(500).json({ ok: false, error: e.message });
+ }
+});
+
+// Upload JSON export
+app.post('/api/upload', (req, res) => {
+ try {
+ let messages = req.body.messages || req.body;
+ if (!Array.isArray(messages)) {
+ // Try to parse as Slack export format (array of messages)
+ if (typeof messages === 'string') {
+ messages = JSON.parse(messages);
+ }
+ if (!Array.isArray(messages)) {
+ return res.status(400).json({ ok: false, error: 'Expected an array of messages' });
+ }
+ }
+
+ // Filter to Steve's messages only
+ const steveMessages = messages.filter(m => {
+ // Slack export format has user field or user_profile
+ const userId = m.user || (m.user_profile && m.user_profile.real_name);
+ return userId === STEVE_USER_ID ||
+ (typeof userId === 'string' && userId.toLowerCase().includes('steve')) ||
+ m.subtype === undefined; // In self-DM, all messages are from Steve
+ });
+
+ const cfg = loadConfig();
+ cfg.cachedMessages = steveMessages.length > 0 ? steveMessages : messages;
+ cfg.lastUpload = new Date().toISOString();
+ saveConfig(cfg);
+
+ res.json({ ok: true, count: cfg.cachedMessages.length, source: 'upload' });
+ } catch (e) {
+ res.status(400).json({ ok: false, error: e.message });
+ }
+});
+
+// Get cached messages
+app.get('/api/cached', (req, res) => {
+ const cfg = loadConfig();
+ res.json({
+ ok: true,
+ messages: cfg.cachedMessages || [],
+ count: (cfg.cachedMessages || []).length,
+ channelId: cfg.channelId,
+ lastFetch: cfg.lastFetch,
+ lastUpload: cfg.lastUpload
+ });
+});
+
+// Clear cache
+app.delete('/api/cached', (req, res) => {
+ const cfg = loadConfig();
+ cfg.cachedMessages = [];
+ saveConfig(cfg);
+ res.json({ ok: true });
+});
+
+// Serve frontend
+app.get('/', (req, res) => {
+ res.send(getHTML());
+});
+
+// --- Frontend ---
+function getHTML() {
+ return `<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Steve's Notes to Self | Slacky</title>
+ <style>
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+ body {
+ background: #0f1117;
+ color: #e0e0e0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ min-height: 100vh;
+ }
+
+ /* Agent Avatar */
+ .agent-avatar { width: 56px; height: 56px; border-radius: 50%; border: 2px solid #4A154B; object-fit: cover; }
+ .agent-avatar-fallback { width: 56px; height: 56px; border-radius: 50%; border: 2px solid #4A154B; background: #4A154B; color: #fff; font-size: 22px; font-weight: 700; display: none; align-items: center; justify-content: center; }
+
+ /* Header */
+ .header {
+ background: #1a1d27;
+ border-bottom: 1px solid #2d3041;
+ padding: 16px 24px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ }
+ .header-left {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ }
+ .header-icon {
+ width: 36px;
+ height: 36px;
+ background: linear-gradient(135deg, #4a154b, #611f69);
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 18px;
+ }
+ .header h1 {
+ font-size: 18px;
+ font-weight: 600;
+ color: #fff;
+ }
+ .header .subtitle {
+ font-size: 12px;
+ color: #8b8fa3;
+ margin-top: 2px;
+ }
+ .header-right {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ }
+ .status-badge {
+ padding: 4px 10px;
+ border-radius: 12px;
+ font-size: 11px;
+ font-weight: 500;
+ }
+ .status-connected { background: #1a3a2a; color: #4ade80; }
+ .status-offline { background: #3a1a1a; color: #f87171; }
+ .status-cached { background: #3a2a1a; color: #fbbf24; }
+
+ /* Controls */
+ .controls {
+ background: #1a1d27;
+ border-bottom: 1px solid #2d3041;
+ padding: 12px 24px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-wrap: wrap;
+ }
+ .search-box {
+ flex: 1;
+ min-width: 200px;
+ position: relative;
+ }
+ .search-box input {
+ width: 100%;
+ padding: 8px 12px 8px 36px;
+ background: #0f1117;
+ border: 1px solid #2d3041;
+ border-radius: 6px;
+ color: #e0e0e0;
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.2s;
+ }
+ .search-box input:focus {
+ border-color: #611f69;
+ }
+ .search-box input::placeholder {
+ color: #555;
+ }
+ .search-icon {
+ position: absolute;
+ left: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: #555;
+ font-size: 14px;
+ }
+
+ .btn {
+ padding: 8px 16px;
+ border-radius: 6px;
+ border: none;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s;
+ white-space: nowrap;
+ }
+ .btn-primary {
+ background: linear-gradient(135deg, #4a154b, #611f69);
+ color: #fff;
+ }
+ .btn-primary:hover { background: linear-gradient(135deg, #611f69, #7c2d7e); }
+ .btn-secondary {
+ background: #2d3041;
+ color: #e0e0e0;
+ }
+ .btn-secondary:hover { background: #3d4055; }
+ .btn-danger {
+ background: #3a1a1a;
+ color: #f87171;
+ }
+ .btn-danger:hover { background: #4a2020; }
+ .btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ .msg-count {
+ font-size: 12px;
+ color: #8b8fa3;
+ white-space: nowrap;
+ }
+
+ /* Main content */
+ .main {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+ }
+
+ /* Setup panel */
+ .setup-panel {
+ background: #1a1d27;
+ border: 1px solid #2d3041;
+ border-radius: 12px;
+ padding: 24px;
+ margin-bottom: 20px;
+ }
+ .setup-panel h2 {
+ font-size: 16px;
+ margin-bottom: 12px;
+ color: #fff;
+ }
+ .setup-panel p {
+ font-size: 13px;
+ color: #8b8fa3;
+ margin-bottom: 16px;
+ line-height: 1.5;
+ }
+ .setup-row {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 12px;
+ }
+ .setup-row input {
+ flex: 1;
+ padding: 8px 12px;
+ background: #0f1117;
+ border: 1px solid #2d3041;
+ border-radius: 6px;
+ color: #e0e0e0;
+ font-size: 14px;
+ outline: none;
+ }
+ .setup-row input:focus { border-color: #611f69; }
+ .setup-or {
+ text-align: center;
+ color: #555;
+ font-size: 12px;
+ margin: 16px 0;
+ position: relative;
+ }
+ .setup-or::before, .setup-or::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ width: 40%;
+ height: 1px;
+ background: #2d3041;
+ }
+ .setup-or::before { left: 0; }
+ .setup-or::after { right: 0; }
+
+ .upload-zone {
+ border: 2px dashed #2d3041;
+ border-radius: 8px;
+ padding: 24px;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.2s;
+ }
+ .upload-zone:hover { border-color: #611f69; background: rgba(97, 31, 105, 0.05); }
+ .upload-zone.dragover { border-color: #611f69; background: rgba(97, 31, 105, 0.1); }
+ .upload-icon { font-size: 32px; margin-bottom: 8px; }
+ .upload-text { font-size: 13px; color: #8b8fa3; }
+ .upload-text strong { color: #e0e0e0; }
+
+ /* Messages */
+ .messages-container {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+
+ /* Date separator */
+ .date-separator {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin: 16px 0 8px;
+ }
+ .date-separator .line {
+ flex: 1;
+ height: 1px;
+ background: #2d3041;
+ }
+ .date-separator .date-label {
+ font-size: 12px;
+ font-weight: 600;
+ color: #8b8fa3;
+ background: #0f1117;
+ padding: 2px 12px;
+ border-radius: 12px;
+ border: 1px solid #2d3041;
+ white-space: nowrap;
+ }
+
+ /* Chat bubble */
+ .message {
+ display: flex;
+ gap: 10px;
+ padding: 6px 16px;
+ border-radius: 4px;
+ transition: background 0.15s;
+ }
+ .message:hover {
+ background: rgba(255,255,255,0.02);
+ }
+ .message.highlight {
+ background: rgba(97, 31, 105, 0.15);
+ border-left: 3px solid #611f69;
+ }
+ .avatar {
+ width: 36px;
+ height: 36px;
+ border-radius: 6px;
+ background: linear-gradient(135deg, #2563eb, #1d4ed8);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 14px;
+ font-weight: 700;
+ color: #fff;
+ flex-shrink: 0;
+ margin-top: 2px;
+ }
+ .message-content {
+ flex: 1;
+ min-width: 0;
+ }
+ .message-header {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ margin-bottom: 2px;
+ }
+ .sender-name {
+ font-size: 14px;
+ font-weight: 700;
+ color: #fff;
+ }
+ .message-time {
+ font-size: 11px;
+ color: #555;
+ }
+ .message-text {
+ font-size: 14px;
+ line-height: 1.5;
+ color: #d4d4d8;
+ word-break: break-word;
+ }
+ .message-text a {
+ color: #60a5fa;
+ text-decoration: none;
+ }
+ .message-text a:hover { text-decoration: underline; }
+ .message-text code {
+ background: #2d3041;
+ padding: 1px 4px;
+ border-radius: 3px;
+ font-family: 'SF Mono', Monaco, Consolas, monospace;
+ font-size: 12px;
+ color: #f0abfc;
+ }
+ .message-text pre {
+ background: #1a1d27;
+ border: 1px solid #2d3041;
+ border-radius: 6px;
+ padding: 12px;
+ margin: 8px 0;
+ overflow-x: auto;
+ font-family: 'SF Mono', Monaco, Consolas, monospace;
+ font-size: 12px;
+ color: #d4d4d8;
+ }
+ .message-text blockquote {
+ border-left: 3px solid #4a154b;
+ padding-left: 12px;
+ margin: 4px 0;
+ color: #8b8fa3;
+ }
+
+ /* Files/attachments */
+ .attachment {
+ margin-top: 6px;
+ padding: 8px 12px;
+ background: #1a1d27;
+ border: 1px solid #2d3041;
+ border-left: 3px solid #611f69;
+ border-radius: 4px;
+ font-size: 13px;
+ }
+ .attachment-title {
+ color: #60a5fa;
+ font-weight: 500;
+ margin-bottom: 4px;
+ }
+ .attachment-text {
+ color: #8b8fa3;
+ font-size: 12px;
+ }
+ .attachment img {
+ max-width: 300px;
+ max-height: 200px;
+ border-radius: 4px;
+ margin-top: 6px;
+ }
+
+ /* Loading / Empty */
+ .empty-state {
+ text-align: center;
+ padding: 60px 20px;
+ color: #555;
+ }
+ .empty-state .icon { font-size: 48px; margin-bottom: 16px; }
+ .empty-state h3 { font-size: 18px; color: #8b8fa3; margin-bottom: 8px; }
+ .empty-state p { font-size: 13px; }
+
+ .loading {
+ text-align: center;
+ padding: 40px;
+ color: #8b8fa3;
+ }
+ .spinner {
+ width: 32px;
+ height: 32px;
+ border: 3px solid #2d3041;
+ border-top-color: #611f69;
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+ margin: 0 auto 12px;
+ }
+ @keyframes spin { to { transform: rotate(360deg); } }
+
+ /* Toast */
+ .toast {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ padding: 12px 20px;
+ border-radius: 8px;
+ font-size: 13px;
+ z-index: 1000;
+ transition: opacity 0.3s;
+ max-width: 400px;
+ }
+ .toast-success { background: #1a3a2a; color: #4ade80; border: 1px solid #22543d; }
+ .toast-error { background: #3a1a1a; color: #f87171; border: 1px solid #7f1d1d; }
+ .toast-info { background: #1a2a3a; color: #60a5fa; border: 1px solid #1e3a5f; }
+
+ /* Responsive */
+ @media (max-width: 640px) {
+ .header { padding: 12px 16px; }
+ .controls { padding: 8px 16px; }
+ .main { padding: 12px; }
+ .message { padding: 6px 8px; }
+ .avatar { width: 28px; height: 28px; font-size: 11px; }
+ }
+
+ /* Hidden file input */
+ #fileInput { display: none; }
+ </style>
+</head>
+<body>
+ <div id="root"></div>
+
+ <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
+ <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
+ <script crossorigin src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
+
+ <script type="text/babel">
+ const { useState, useEffect, useRef, useMemo, useCallback } = React;
+
+ // --- Helpers ---
+ function formatTimestamp(ts) {
+ if (!ts) return '';
+ const d = new Date(typeof ts === 'string' && ts.includes('.') ? parseFloat(ts) * 1000 :
+ typeof ts === 'number' ? ts * 1000 : ts);
+ if (isNaN(d.getTime())) return ts;
+ return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
+ }
+
+ function formatDate(ts) {
+ if (!ts) return '';
+ const d = new Date(typeof ts === 'string' && ts.includes('.') ? parseFloat(ts) * 1000 :
+ typeof ts === 'number' ? ts * 1000 : ts);
+ if (isNaN(d.getTime())) return '';
+ const today = new Date();
+ const yesterday = new Date(today);
+ yesterday.setDate(yesterday.getDate() - 1);
+ if (d.toDateString() === today.toDateString()) return 'Today';
+ if (d.toDateString() === yesterday.toDateString()) return 'Yesterday';
+ return d.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });
+ }
+
+ function getDateKey(ts) {
+ if (!ts) return 'unknown';
+ const d = new Date(typeof ts === 'string' && ts.includes('.') ? parseFloat(ts) * 1000 :
+ typeof ts === 'number' ? ts * 1000 : ts);
+ if (isNaN(d.getTime())) return 'unknown';
+ return d.toDateString();
+ }
+
+ function formatMessageText(text) {
+ if (!text) return '';
+ // Convert Slack mrkdwn to HTML
+ let html = text
+ // Escape HTML
+ .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
+ // URLs
+ .replace(/<(https?:\/\/[^|>]+)\|([^>]+)>/g, '<a href="$1" target="_blank" rel="noopener">$2</a>')
+ .replace(/<(https?:\/\/[^>]+)>/g, '<a href="$1" target="_blank" rel="noopener">$1</a>')
+ // Bold
+ .replace(/\*([^*]+)\*/g, '<strong>$1</strong>')
+ // Italic
+ .replace(/_([^_]+)_/g, '<em>$1</em>')
+ // Strikethrough
+ .replace(/~([^~]+)~/g, '<del>$1</del>')
+ // Inline code
+ .replace(/\`([^\`]+)\`/g, '<code>$1</code>')
+ // Code blocks
+ .replace(/\`\`\`([\\s\\S]*?)\`\`\`/g, '<pre>$1</pre>')
+ // Blockquote
+ .replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
+ // Newlines
+ .replace(/\\n/g, '<br>');
+ return html;
+ }
+
+ // --- Toast Component ---
+ function Toast({ message, type, onClose }) {
+ useEffect(() => {
+ const timer = setTimeout(onClose, 4000);
+ return () => clearTimeout(timer);
+ }, [onClose]);
+
+ return (
+ <div className={"toast toast-" + type}>
+ {message}
+ </div>
+ );
+ }
+
+ // --- Main App ---
+ function App() {
+ const [messages, setMessages] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [search, setSearch] = useState('');
+ const [channelId, setChannelId] = useState('');
+ const [channelInput, setChannelInput] = useState('');
+ const [showSetup, setShowSetup] = useState(true);
+ const [source, setSource] = useState('');
+ const [toast, setToast] = useState(null);
+ const fileInputRef = useRef(null);
+
+ const showToast = useCallback((message, type = 'info') => {
+ setToast({ message, type });
+ }, []);
+
+ // Load config on mount
+ useEffect(() => {
+ fetch('/api/config', { headers: { 'Authorization': 'Basic ' + btoa('admin:I3YusisdESUNdxtrmlb3QJeu9q8ODKJO') } })
+ .then(r => r.json())
+ .then(cfg => {
+ if (cfg.channelId) {
+ setChannelId(cfg.channelId);
+ setChannelInput(cfg.channelId);
+ setShowSetup(false);
+ // Load cached messages
+ loadMessages(cfg.channelId);
+ }
+ })
+ .catch(() => {});
+ }, []);
+
+ const loadMessages = useCallback(async (chId) => {
+ setLoading(true);
+ try {
+ const params = chId ? '?channel=' + chId : '';
+ const res = await fetch('/api/messages' + params, {
+ headers: { 'Authorization': 'Basic ' + btoa('admin:I3YusisdESUNdxtrmlb3QJeu9q8ODKJO') }
+ });
+ const data = await res.json();
+ if (data.ok) {
+ setMessages(data.messages || []);
+ setSource(data.source || 'unknown');
+ setShowSetup(false);
+ showToast('Loaded ' + (data.messages || []).length + ' messages from ' + (data.source || 'Slack'), 'success');
+ } else {
+ // Try cached
+ const cachedRes = await fetch('/api/cached', {
+ headers: { 'Authorization': 'Basic ' + btoa('admin:I3YusisdESUNdxtrmlb3QJeu9q8ODKJO') }
+ });
+ const cachedData = await cachedRes.json();
+ if (cachedData.messages && cachedData.messages.length > 0) {
+ setMessages(cachedData.messages);
+ setSource('cache');
+ setShowSetup(false);
+ showToast('Loaded ' + cachedData.messages.length + ' cached messages. API error: ' + data.error, 'info');
+ } else {
+ showToast(data.error || 'Failed to load messages', 'error');
+ }
+ }
+ } catch (e) {
+ showToast('Error: ' + e.message, 'error');
+ } finally {
+ setLoading(false);
+ }
+ }, [showToast]);
+
+ const handleSetChannel = useCallback(async () => {
+ if (!channelInput.trim()) return;
+ try {
+ const res = await fetch('/api/config/channel', {
+ method: 'POST',
+ headers: {
+ 'Authorization': 'Basic ' + btoa('admin:I3YusisdESUNdxtrmlb3QJeu9q8ODKJO'),
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ channelId: channelInput.trim() })
+ });
+ const data = await res.json();
+ if (data.ok) {
+ setChannelId(channelInput.trim());
+ showToast('Channel ID set. Loading messages...', 'success');
+ loadMessages(channelInput.trim());
+ }
+ } catch (e) {
+ showToast('Error: ' + e.message, 'error');
+ }
+ }, [channelInput, loadMessages, showToast]);
+
+ const handleAutoDetect = useCallback(async () => {
+ setLoading(true);
+ try {
+ const res = await fetch('/api/detect-channel', {
+ method: 'POST',
+ headers: { 'Authorization': 'Basic ' + btoa('admin:I3YusisdESUNdxtrmlb3QJeu9q8ODKJO') }
+ });
+ const data = await res.json();
+ if (data.ok) {
+ setChannelId(data.channelId);
+ setChannelInput(data.channelId);
+ showToast('Auto-detected channel: ' + data.channelId, 'success');
+ loadMessages(data.channelId);
+ } else {
+ showToast(data.hint || data.error || 'Auto-detect failed', 'error');
+ }
+ } catch (e) {
+ showToast('Error: ' + e.message, 'error');
+ } finally {
+ setLoading(false);
+ }
+ }, [loadMessages, showToast]);
+
+ const handleFileUpload = useCallback(async (e) => {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ setLoading(true);
+ try {
+ const text = await file.text();
+ let parsed;
+ try {
+ parsed = JSON.parse(text);
+ } catch {
+ showToast('Invalid JSON file', 'error');
+ setLoading(false);
+ return;
+ }
+
+ const res = await fetch('/api/upload', {
+ method: 'POST',
+ headers: {
+ 'Authorization': 'Basic ' + btoa('admin:I3YusisdESUNdxtrmlb3QJeu9q8ODKJO'),
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ messages: parsed })
+ });
+ const data = await res.json();
+ if (data.ok) {
+ showToast('Uploaded ' + data.count + ' messages', 'success');
+ // Reload from cache
+ const cachedRes = await fetch('/api/cached', {
+ headers: { 'Authorization': 'Basic ' + btoa('admin:I3YusisdESUNdxtrmlb3QJeu9q8ODKJO') }
+ });
+ const cachedData = await cachedRes.json();
+ setMessages(cachedData.messages || []);
+ setSource('upload');
+ setShowSetup(false);
+ } else {
+ showToast(data.error || 'Upload failed', 'error');
+ }
+ } catch (e) {
+ showToast('Error: ' + e.message, 'error');
+ } finally {
+ setLoading(false);
+ e.target.value = '';
+ }
+ }, [showToast]);
+
+ const handleDrop = useCallback((e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const file = e.dataTransfer.files[0];
+ if (file && file.name.endsWith('.json')) {
+ // Simulate file input change
+ const dt = new DataTransfer();
+ dt.items.add(file);
+ fileInputRef.current.files = dt.files;
+ fileInputRef.current.dispatchEvent(new Event('change', { bubbles: true }));
+ }
+ }, []);
+
+ // Filter messages by search
+ const filteredMessages = useMemo(() => {
+ if (!search.trim()) return messages;
+ const q = search.toLowerCase();
+ return messages.filter(m => {
+ const text = (m.text || '').toLowerCase();
+ return text.includes(q);
+ });
+ }, [messages, search]);
+
+ // Group messages by date
+ const groupedMessages = useMemo(() => {
+ const groups = {};
+ // Messages from Slack API come newest first, we want to display newest first too
+ const sorted = [...filteredMessages].sort((a, b) => {
+ const tsA = parseFloat(a.ts || 0);
+ const tsB = parseFloat(b.ts || 0);
+ return tsB - tsA;
+ });
+
+ sorted.forEach(m => {
+ const key = getDateKey(m.ts);
+ if (!groups[key]) groups[key] = { date: m.ts, messages: [] };
+ groups[key].messages.push(m);
+ });
+
+ return Object.values(groups);
+ }, [filteredMessages]);
+
+ const statusType = source === 'slack-api' ? 'connected' :
+ source === 'upload' || source === 'cache' ? 'cached' : 'offline';
+ const statusText = source === 'slack-api' ? 'Live from Slack' :
+ source === 'upload' ? 'From Upload' :
+ source === 'cache' ? 'Cached' :
+ source === 'cache-fallback' ? 'Cached (API Error)' : 'Not Connected';
+
+ return (
+ <div>
+ {/* Header */}
+ <div className="header">
+ <div className="header-left">
+ <img className="agent-avatar" src="http://45.61.58.125/agent-avatars/the-stager.png" onError="this.style.display='none';this.nextElementSibling.style.display='flex'" />
+ <div className="agent-avatar-fallback">S</div>
+ <div className="header-icon">S</div>
+ <div>
+ <h1>Steve's Notes to Self</h1>
+ <div className="subtitle">Slacky</div>
+ </div>
+ </div>
+ <div className="header-right">
+ <span className={"status-badge status-" + statusType}>{statusText}</span>
+ <button className="btn btn-secondary" onClick={() => setShowSetup(!showSetup)}>
+ {showSetup ? 'Hide Setup' : 'Setup'}
+ </button>
+ </div>
+ </div>
+
+ {/* Controls */}
+ <div className="controls">
+ <div className="search-box">
+ <span className="search-icon">🔍</span>
+ <input
+ type="text"
+ placeholder="Search messages..."
+ value={search}
+ onChange={e => setSearch(e.target.value)}
+ />
+ </div>
+ <span className="msg-count">
+ {filteredMessages.length === messages.length
+ ? messages.length + ' messages'
+ : filteredMessages.length + ' of ' + messages.length + ' messages'}
+ </span>
+ {channelId && (
+ <button className="btn btn-primary" onClick={() => loadMessages(channelId)} disabled={loading}>
+ {loading ? 'Loading...' : 'Refresh'}
+ </button>
+ )}
+ </div>
+
+ <div className="main">
+ {/* Setup Panel */}
+ {showSetup && (
+ <div className="setup-panel">
+ <h2>Connect to Slack</h2>
+ <p>
+ To view your self-DMs, enter your Slack DM channel ID below.<br/>
+ <strong>How to find it:</strong> In Slack, open your self-DM (message yourself).
+ Click the channel name at the top, then look at the URL in your browser.
+ The channel ID starts with <code>D</code> and looks like <code>D0XXXXXXX</code>.
+ </p>
+
+ <div className="setup-row">
+ <input
+ type="text"
+ placeholder="Channel ID (e.g., D03U65XXXXX)"
+ value={channelInput}
+ onChange={e => setChannelInput(e.target.value)}
+ onKeyDown={e => e.key === 'Enter' && handleSetChannel()}
+ />
+ <button className="btn btn-primary" onClick={handleSetChannel} disabled={loading}>
+ Connect
+ </button>
+ <button className="btn btn-secondary" onClick={handleAutoDetect} disabled={loading}>
+ Auto-Detect
+ </button>
+ </div>
+
+ <div className="setup-or">OR</div>
+
+ <div
+ className="upload-zone"
+ onClick={() => fileInputRef.current && fileInputRef.current.click()}
+ onDrop={handleDrop}
+ onDragOver={e => { e.preventDefault(); e.currentTarget.classList.add('dragover'); }}
+ onDragLeave={e => e.currentTarget.classList.remove('dragover')}
+ >
+ <div className="upload-icon">📤</div>
+ <div className="upload-text">
+ <strong>Upload Slack Export JSON</strong><br/>
+ Drag and drop or click to browse.<br/>
+ Export from Slack: Workspace Settings > Import/Export > Export.
+ </div>
+ </div>
+ <input
+ ref={fileInputRef}
+ id="fileInput"
+ type="file"
+ accept=".json"
+ onChange={handleFileUpload}
+ />
+ </div>
+ )}
+
+ {/* Loading */}
+ {loading && (
+ <div className="loading">
+ <div className="spinner"></div>
+ Fetching messages from Slack...
+ </div>
+ )}
+
+ {/* Empty state */}
+ {!loading && messages.length === 0 && !showSetup && (
+ <div className="empty-state">
+ <div className="icon">💬</div>
+ <h3>No Messages Yet</h3>
+ <p>Connect to Slack or upload a JSON export to view your notes.</p>
+ <button className="btn btn-primary" style={{marginTop: '16px'}} onClick={() => setShowSetup(true)}>
+ Setup Connection
+ </button>
+ </div>
+ )}
+
+ {/* No search results */}
+ {!loading && messages.length > 0 && filteredMessages.length === 0 && (
+ <div className="empty-state">
+ <div className="icon">🔎</div>
+ <h3>No Results</h3>
+ <p>No messages match "{search}"</p>
+ </div>
+ )}
+
+ {/* Messages */}
+ {!loading && filteredMessages.length > 0 && (
+ <div className="messages-container">
+ {groupedMessages.map((group, gi) => (
+ <div key={gi}>
+ <div className="date-separator">
+ <div className="line"></div>
+ <span className="date-label">{formatDate(group.date)}</span>
+ <div className="line"></div>
+ </div>
+ {group.messages.map((msg, mi) => (
+ <div
+ key={msg.ts || mi}
+ className={"message" + (search && msg.text && msg.text.toLowerCase().includes(search.toLowerCase()) ? ' highlight' : '')}
+ >
+ <div className="avatar">SA</div>
+ <div className="message-content">
+ <div className="message-header">
+ <span className="sender-name">Steve Abrams</span>
+ <span className="message-time">{formatTimestamp(msg.ts)}</span>
+ </div>
+ <div
+ className="message-text"
+ dangerouslySetInnerHTML={{ __html: formatMessageText(msg.text) }}
+ />
+ {/* Attachments */}
+ {msg.attachments && msg.attachments.map((att, ai) => (
+ <div key={ai} className="attachment">
+ {att.title && <div className="attachment-title">{att.title}</div>}
+ {att.text && <div className="attachment-text">{att.text}</div>}
+ {att.fallback && !att.title && !att.text && <div className="attachment-text">{att.fallback}</div>}
+ {att.image_url && <img src={att.image_url} alt="attachment" />}
+ {att.thumb_url && !att.image_url && <img src={att.thumb_url} alt="attachment" />}
+ </div>
+ ))}
+ {/* Files */}
+ {msg.files && msg.files.map((file, fi) => (
+ <div key={fi} className="attachment">
+ <div className="attachment-title">
+ {file.url_private ? (
+ <a href={file.url_private} target="_blank" rel="noopener">{file.name || file.title || 'File'}</a>
+ ) : (
+ file.name || file.title || 'File'
+ )}
+ </div>
+ {file.mimetype && file.mimetype.startsWith('image/') && file.url_private && (
+ <img src={file.url_private} alt={file.name} />
+ )}
+ </div>
+ ))}
+ </div>
+ </div>
+ ))}
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+
+ {/* Toast */}
+ {toast && (
+ <Toast
+ message={toast.message}
+ type={toast.type}
+ onClose={() => setToast(null)}
+ />
+ )}
+ </div>
+ );
+ }
+
+ ReactDOM.createRoot(document.getElementById('root')).render(<App />);
+ </script>
+</body>
+</html>`;
+}
+
+// --- Claude-Steve Channel Viewer ---
+
+// Fetch messages from #claude-to-steve (Steve's messages only, no bots)
+app.get('/api/claude-steve/messages', async (req, res) => {
+ try {
+ let allMessages = [];
+ let cursor = undefined;
+ let pages = 0;
+ const maxPages = 50;
+
+ do {
+ const params = { channel: CLAUDE_STEVE_CHANNEL, limit: '200' };
+ if (cursor) params.cursor = cursor;
+
+ const result = await slackApi('conversations.history', params, SLACK_USER_TOKEN);
+
+ if (!result.ok) {
+ return res.status(400).json({ ok: false, error: result.error });
+ }
+
+ const steveMessages = result.messages.filter(m =>
+ m.user === STEVE_USER_ID && !m.subtype && !m.bot_id
+ );
+ allMessages = allMessages.concat(steveMessages);
+
+ cursor = result.response_metadata && result.response_metadata.next_cursor;
+ pages++;
+ } while (cursor && pages < maxPages);
+
+ allMessages.sort((a, b) => parseFloat(b.ts) - parseFloat(a.ts));
+ res.json({ ok: true, messages: allMessages, count: allMessages.length, pages });
+ } catch (e) {
+ res.status(500).json({ ok: false, error: e.message });
+ }
+});
+
+// Serve Claude-Steve viewer
+app.get('/claude-steve', (req, res) => {
+ res.send(getClaudeSteveHTML());
+});
+
+// Claude-Steve viewer frontend (inline)
+function getClaudeSteveHTML() {
+ return fs.readFileSync(path.join(__dirname, 'claude-steve.html'), 'utf8');
+}
+
+// --- Start ---
+app.listen(PORT, '0.0.0.0', () => {
+ console.log(`Slacky running on http://0.0.0.0:${PORT}`);
+ console.log(`Claude-Steve viewer: http://45.61.58.125:${PORT}/claude-steve`);
+ console.log(`Dashboard: http://45.61.58.125:${PORT}`);
+});
(oldest)
·
back to Slack Dm Viewer
·
snapshot: 1 file(s) changed, ~1 modified cabe9d5 →