dashboard.html 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Switchboard Dashboard</title>
  5. <meta charset="UTF-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <style>
  8. * { box-sizing: border-box; margin: 0; padding: 0; }
  9. body {
  10. font-family: 'Courier New', 'Consolas', monospace;
  11. background: #f5f5dc;
  12. color: #000;
  13. padding: 15px;
  14. line-height: 1.4;
  15. font-weight: 600;
  16. }
  17. .container { max-width: 1400px; margin: 0 auto; background: white; padding: 30px; box-shadow: 0 0 20px rgba(0,0,0,0.1); }
  18. h1 {
  19. color: #000;
  20. margin-bottom: 8px;
  21. font-size: 24px;
  22. letter-spacing: 3px;
  23. font-weight: 900;
  24. text-transform: uppercase;
  25. }
  26. h2 {
  27. color: #000;
  28. margin: 20px 0 10px;
  29. font-size: 16px;
  30. letter-spacing: 2px;
  31. font-weight: 900;
  32. text-transform: uppercase;
  33. border-bottom: 3px solid #000;
  34. padding-bottom: 3px;
  35. }
  36. /* API Key Section */
  37. #api-key-section {
  38. background: #fafafa;
  39. border: 3px solid #000;
  40. padding: 15px;
  41. margin-bottom: 20px;
  42. }
  43. #api-key-section input {
  44. width: 100%;
  45. padding: 8px;
  46. border: 2px solid #666;
  47. background: white;
  48. color: #000;
  49. font-family: 'Courier New', monospace;
  50. font-size: 14px;
  51. font-weight: 600;
  52. }
  53. #api-key-section input:focus {
  54. outline: none;
  55. border-color: #000;
  56. box-shadow: none;
  57. }
  58. /* Buttons */
  59. button {
  60. background: white;
  61. color: #000;
  62. border: 3px solid #000;
  63. padding: 8px 16px;
  64. cursor: pointer;
  65. font-size: 13px;
  66. font-family: 'Courier New', monospace;
  67. margin-top: 8px;
  68. text-transform: uppercase;
  69. letter-spacing: 1px;
  70. font-weight: 900;
  71. }
  72. button:hover {
  73. background: #000;
  74. color: white;
  75. }
  76. button.danger {
  77. border-color: #000;
  78. color: #000;
  79. }
  80. button.danger:hover {
  81. background: #000;
  82. color: white;
  83. }
  84. /* Tabs */
  85. .tabs {
  86. display: flex;
  87. gap: 2px;
  88. margin-bottom: 0;
  89. border-bottom: 3px solid #000;
  90. }
  91. .tab {
  92. padding: 10px 20px;
  93. background: #e8e8e8;
  94. color: #000;
  95. border: 3px solid #000;
  96. border-bottom: none;
  97. cursor: pointer;
  98. font-weight: 900;
  99. font-family: 'Courier New', monospace;
  100. text-transform: uppercase;
  101. letter-spacing: 1px;
  102. font-size: 13px;
  103. }
  104. .tab:hover { background: #d0d0d0; }
  105. .tab.active {
  106. background: white;
  107. color: #000;
  108. border-bottom: 3px solid white;
  109. margin-bottom: -3px;
  110. }
  111. /* Content */
  112. .content {
  113. background: white;
  114. border: 3px solid #000;
  115. border-top: none;
  116. padding: 15px;
  117. display: none;
  118. }
  119. .content.active { display: block; }
  120. /* Tables */
  121. table {
  122. width: 100%;
  123. border-collapse: collapse;
  124. margin-top: 10px;
  125. border: 3px solid #000;
  126. }
  127. th, td {
  128. padding: 6px 10px;
  129. text-align: left;
  130. border: 2px solid #000;
  131. }
  132. th {
  133. background: #f0f0f0;
  134. font-weight: 900;
  135. color: #000;
  136. text-transform: uppercase;
  137. font-size: 12px;
  138. letter-spacing: 1px;
  139. }
  140. tr:hover { background: #fafafa; }
  141. td { font-size: 14px; font-weight: 600; }
  142. /* Forms */
  143. .form-group {
  144. margin-bottom: 15px;
  145. }
  146. label {
  147. display: block;
  148. margin-bottom: 6px;
  149. font-weight: 900;
  150. color: #000;
  151. text-transform: uppercase;
  152. font-size: 12px;
  153. letter-spacing: 1px;
  154. }
  155. input[type="text"], input[type="datetime-local"], textarea, select {
  156. width: 100%;
  157. padding: 8px;
  158. border: 2px solid #666;
  159. background: white;
  160. color: #000;
  161. font-family: 'Courier New', monospace;
  162. font-size: 15px;
  163. font-weight: 600;
  164. }
  165. input:focus, textarea:focus, select:focus {
  166. outline: none;
  167. border-color: #000;
  168. box-shadow: none;
  169. }
  170. textarea { min-height: 80px; }
  171. small { color: #666; font-size: 12px; font-weight: 600; }
  172. /* Status badges */
  173. .badge {
  174. padding: 3px 8px;
  175. border: 2px solid;
  176. font-size: 11px;
  177. font-weight: 900;
  178. font-family: 'Courier New', monospace;
  179. text-transform: uppercase;
  180. letter-spacing: 1px;
  181. }
  182. .badge.success {
  183. border-color: #000;
  184. color: #000;
  185. background: white;
  186. }
  187. .badge.failed {
  188. border-color: #000;
  189. color: white;
  190. background: #000;
  191. }
  192. .hidden { display: none; }
  193. .error { color: #000; margin-top: 8px; font-weight: 900; }
  194. .empty { text-align: center; padding: 30px; color: #666; font-weight: 600; }
  195. /* Loading spinner */
  196. .loading {
  197. text-align: center;
  198. padding: 40px;
  199. color: #000;
  200. }
  201. .spinner {
  202. display: inline-block;
  203. width: 40px;
  204. height: 40px;
  205. border: 4px solid #e0e0e0;
  206. border-top-color: #000;
  207. border-radius: 50%;
  208. animation: spin 1s linear infinite;
  209. margin-bottom: 10px;
  210. }
  211. @keyframes spin {
  212. to { transform: rotate(360deg); }
  213. }
  214. @keyframes blink {
  215. 0%, 50% { opacity: 1; }
  216. 51%, 100% { opacity: 0.3; }
  217. }
  218. .blink { animation: blink 1s infinite; }
  219. </style>
  220. </head>
  221. <body>
  222. <div class="container">
  223. <h1>LETTA SWITCHBOARD — MESSAGE ROUTING SERVICE</h1>
  224. <div style="border: 3px solid #000; padding: 12px; margin-bottom: 20px; background: #fafafa;">
  225. <pre style="font-family: 'Courier New', monospace; font-size: 13px; line-height: 1.6; margin: 0; font-weight: 700;">SYSTEM: MESSAGE ROUTING TERMINAL
  226. VERSION: 1.0.0
  227. STATUS: OPERATIONAL
  228. PROTOCOL: REST API + SCHEDULED EXECUTION</pre>
  229. </div>
  230. <!-- API Key Input -->
  231. <div id="api-key-section">
  232. <label for="api-key-input">AUTHENTICATION</label>
  233. <input type="password" id="api-key-input" placeholder="sk-let-...">
  234. <button onclick="saveApiKey()">SAVE KEY</button>
  235. <button onclick="clearApiKey()" class="danger">CLEAR KEY</button>
  236. <div id="api-key-status"></div>
  237. </div>
  238. <div id="main-content" class="hidden">
  239. <!-- Tabs -->
  240. <div class="tabs">
  241. <button class="tab active" onclick="showTab('schedules')">SCHEDULES</button>
  242. <button class="tab" onclick="showTab('create')">CREATE NEW</button>
  243. <button class="tab" onclick="showTab('results')">RESULTS</button>
  244. </div>
  245. <!-- Schedules Tab -->
  246. <div id="schedules-tab" class="content active">
  247. <h2>ONE-TIME SCHEDULES</h2>
  248. <table id="onetime-table">
  249. <thead>
  250. <tr>
  251. <th>ID</th>
  252. <th>Agent ID</th>
  253. <th>Execute At</th>
  254. <th>Message</th>
  255. <th>Actions</th>
  256. </tr>
  257. </thead>
  258. <tbody></tbody>
  259. </table>
  260. <h2>RECURRING SCHEDULES</h2>
  261. <table id="recurring-table">
  262. <thead>
  263. <tr>
  264. <th>ID</th>
  265. <th>Agent ID</th>
  266. <th>Cron</th>
  267. <th>Message</th>
  268. <th>Last Run</th>
  269. <th>Actions</th>
  270. </tr>
  271. </thead>
  272. <tbody></tbody>
  273. </table>
  274. <button onclick="loadSchedules()" style="margin-top: 20px;">REFRESH</button>
  275. </div>
  276. <!-- Create Tab -->
  277. <div id="create-tab" class="content">
  278. <h2>CREATE SCHEDULE</h2>
  279. <div class="form-group">
  280. <label>SCHEDULE TYPE</label>
  281. <select id="schedule-type" onchange="toggleScheduleType()">
  282. <option value="onetime">One-Time</option>
  283. <option value="recurring">Recurring</option>
  284. </select>
  285. </div>
  286. <div class="form-group">
  287. <label for="agent-id">AGENT ID</label>
  288. <input type="text" id="agent-id" placeholder="agent-xxx">
  289. </div>
  290. <div class="form-group">
  291. <label for="message">MESSAGE</label>
  292. <textarea id="message" placeholder="Your message to the agent"></textarea>
  293. </div>
  294. <div id="onetime-fields">
  295. <div class="form-group">
  296. <label for="execute-at">EXECUTE AT</label>
  297. <input type="datetime-local" id="execute-at">
  298. </div>
  299. <div class="form-group">
  300. <label for="timezone">TIMEZONE</label>
  301. <select id="timezone">
  302. <option value="local" selected>Local Browser Time</option>
  303. <option value="UTC">UTC</option>
  304. <option value="America/New_York">Eastern (US)</option>
  305. <option value="America/Chicago">Central (US)</option>
  306. <option value="America/Denver">Mountain (US)</option>
  307. <option value="America/Los_Angeles">Pacific (US)</option>
  308. <option value="Europe/London">London</option>
  309. <option value="Europe/Paris">Paris</option>
  310. <option value="Asia/Tokyo">Tokyo</option>
  311. </select>
  312. <small style="display:block;margin-top:5px;color:#6b7280;">Time will be converted to UTC for storage</small>
  313. </div>
  314. </div>
  315. <div id="recurring-fields" class="hidden">
  316. <div class="form-group">
  317. <label for="cron">CRON EXPRESSION</label>
  318. <input type="text" id="cron" placeholder="0 9 * * 1-5">
  319. <small style="display:block;margin-top:5px;color:#6b7280;">Examples: "0 9 * * *" (daily at 9am), "*/5 * * * *" (every 5 min)</small>
  320. </div>
  321. </div>
  322. <button onclick="createSchedule()">CREATE SCHEDULE</button>
  323. <div id="create-error" class="error"></div>
  324. </div>
  325. <!-- Results Tab -->
  326. <div id="results-tab" class="content">
  327. <h2>EXECUTION RESULTS</h2>
  328. <table id="results-table">
  329. <thead>
  330. <tr>
  331. <th>Schedule ID</th>
  332. <th>Type</th>
  333. <th>Status</th>
  334. <th>Agent ID</th>
  335. <th>Message</th>
  336. <th>Run ID / Error</th>
  337. <th>Executed At</th>
  338. </tr>
  339. </thead>
  340. <tbody></tbody>
  341. </table>
  342. <button onclick="loadResults()" style="margin-top: 20px;">REFRESH</button>
  343. </div>
  344. </div>
  345. </div>
  346. <script>
  347. const API_BASE = window.location.origin;
  348. let API_KEY = sessionStorage.getItem('letta_api_key') || '';
  349. // Initialize
  350. if (API_KEY) {
  351. document.getElementById('api-key-input').value = API_KEY;
  352. document.getElementById('main-content').classList.remove('hidden');
  353. loadSchedules();
  354. loadResults();
  355. }
  356. function saveApiKey() {
  357. API_KEY = document.getElementById('api-key-input').value.trim();
  358. if (!API_KEY) {
  359. alert('ERROR: API KEY REQUIRED');
  360. return;
  361. }
  362. sessionStorage.setItem('letta_api_key', API_KEY);
  363. document.getElementById('main-content').classList.remove('hidden');
  364. document.getElementById('api-key-status').innerHTML = '<p style="color:#000;margin-top:10px;font-weight:bold;">✓ API KEY AUTHENTICATED</p>';
  365. loadSchedules();
  366. loadResults();
  367. }
  368. function clearApiKey() {
  369. sessionStorage.removeItem('letta_api_key');
  370. API_KEY = '';
  371. document.getElementById('api-key-input').value = '';
  372. document.getElementById('main-content').classList.add('hidden');
  373. document.getElementById('api-key-status').innerHTML = '<p style="color:#000;margin-top:10px;font-weight:bold;">✗ SESSION TERMINATED</p>';
  374. }
  375. function showTab(tab) {
  376. document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
  377. document.querySelectorAll('.content').forEach(c => c.classList.remove('active'));
  378. event.target.classList.add('active');
  379. document.getElementById(`${tab}-tab`).classList.add('active');
  380. }
  381. function toggleScheduleType() {
  382. const type = document.getElementById('schedule-type').value;
  383. document.getElementById('onetime-fields').classList.toggle('hidden', type !== 'onetime');
  384. document.getElementById('recurring-fields').classList.toggle('hidden', type !== 'recurring');
  385. }
  386. async function loadSchedules() {
  387. const onetimeBody = document.querySelector('#onetime-table tbody');
  388. const recurringBody = document.querySelector('#recurring-table tbody');
  389. // Show loading spinners
  390. onetimeBody.innerHTML = '<tr><td colspan="5" class="loading"><div class="spinner"></div><p>Loading...</p></td></tr>';
  391. recurringBody.innerHTML = '<tr><td colspan="6" class="loading"><div class="spinner"></div><p>Loading...</p></td></tr>';
  392. try {
  393. const [onetime, recurring] = await Promise.all([
  394. fetch(`${API_BASE}/schedules/one-time`, {
  395. headers: { 'Authorization': `Bearer ${API_KEY}` }
  396. }).then(r => r.json()),
  397. fetch(`${API_BASE}/schedules/recurring`, {
  398. headers: { 'Authorization': `Bearer ${API_KEY}` }
  399. }).then(r => r.json())
  400. ]);
  401. // One-time schedules
  402. onetimeBody.innerHTML = onetime.length ? onetime.map(s => `
  403. <tr>
  404. <td>${s.id.substring(0, 8)}...</td>
  405. <td>${s.agent_id}</td>
  406. <td>${new Date(s.execute_at).toLocaleString()}</td>
  407. <td>${truncate(s.message, 50)}</td>
  408. <td><button class="danger" onclick="deleteSchedule('one-time', '${s.id}')">DEL</button></td>
  409. </tr>
  410. `).join('') : '<tr><td colspan="5" class="empty">No one-time schedules</td></tr>';
  411. // Recurring schedules
  412. const recurringBody = document.querySelector('#recurring-table tbody');
  413. recurringBody.innerHTML = recurring.length ? recurring.map(s => `
  414. <tr>
  415. <td>${s.id.substring(0, 8)}...</td>
  416. <td>${s.agent_id}</td>
  417. <td>${s.cron}</td>
  418. <td>${truncate(s.message, 50)}</td>
  419. <td>${s.last_run ? new Date(s.last_run).toLocaleString() : 'Never'}</td>
  420. <td><button class="danger" onclick="deleteSchedule('recurring', '${s.id}')">DEL</button></td>
  421. </tr>
  422. `).join('') : '<tr><td colspan="6" class="empty">No recurring schedules</td></tr>';
  423. } catch (error) {
  424. alert('ERROR LOADING SCHEDULES: ' + error.message);
  425. }
  426. }
  427. async function loadResults() {
  428. const resultsBody = document.querySelector('#results-table tbody');
  429. // Show loading spinner
  430. resultsBody.innerHTML = '<tr><td colspan="7" class="loading"><div class="spinner"></div><p>Loading...</p></td></tr>';
  431. try {
  432. const results = await fetch(`${API_BASE}/results`, {
  433. headers: { 'Authorization': `Bearer ${API_KEY}` }
  434. }).then(r => r.json());
  435. resultsBody.innerHTML = results.length ? results.map(r => `
  436. <tr>
  437. <td>${r.schedule_id.substring(0, 8)}...</td>
  438. <td>${r.schedule_type}</td>
  439. <td><span class="badge ${r.status}">${r.status}</span></td>
  440. <td>${r.agent_id}</td>
  441. <td>${truncate(r.message, 30)}</td>
  442. <td>${r.run_id || r.error || 'N/A'}</td>
  443. <td>${new Date(r.executed_at).toLocaleString()}</td>
  444. </tr>
  445. `).join('') : '<tr><td colspan="7" class="empty">No execution results</td></tr>';
  446. } catch (error) {
  447. alert('ERROR LOADING RESULTS: ' + error.message);
  448. }
  449. }
  450. async function createSchedule() {
  451. const type = document.getElementById('schedule-type').value;
  452. const agentId = document.getElementById('agent-id').value.trim();
  453. const message = document.getElementById('message').value.trim();
  454. const errorDiv = document.getElementById('create-error');
  455. errorDiv.textContent = '';
  456. if (!agentId || !message) {
  457. errorDiv.textContent = 'Please fill in all fields';
  458. return;
  459. }
  460. try {
  461. const payload = {
  462. agent_id: agentId,
  463. message: message,
  464. role: 'user'
  465. };
  466. if (type === 'onetime') {
  467. const executeAt = document.getElementById('execute-at').value;
  468. const timezone = document.getElementById('timezone').value;
  469. if (!executeAt) {
  470. errorDiv.textContent = 'Please specify execution time';
  471. return;
  472. }
  473. // Convert datetime-local to ISO 8601 with timezone handling
  474. let executeDate;
  475. if (timezone === 'local') {
  476. // Use local browser timezone
  477. executeDate = new Date(executeAt);
  478. } else if (timezone === 'UTC') {
  479. // Treat input as UTC
  480. executeDate = new Date(executeAt + 'Z');
  481. } else {
  482. // For named timezones, we assume the datetime-local is in that timezone
  483. // Convert to UTC by parsing as local and adjusting
  484. executeDate = new Date(executeAt);
  485. // Note: This is a simplification. For accurate timezone conversion,
  486. // we'd need a library, but for now we assume UTC or local
  487. }
  488. payload.execute_at = executeDate.toISOString();
  489. } else {
  490. const cron = document.getElementById('cron').value.trim();
  491. if (!cron) {
  492. errorDiv.textContent = 'Please specify cron expression';
  493. return;
  494. }
  495. payload.cron = cron;
  496. }
  497. const endpoint = type === 'onetime' ? '/schedules/one-time' : '/schedules/recurring';
  498. const response = await fetch(`${API_BASE}${endpoint}`, {
  499. method: 'POST',
  500. headers: {
  501. 'Authorization': `Bearer ${API_KEY}`,
  502. 'Content-Type': 'application/json'
  503. },
  504. body: JSON.stringify(payload)
  505. });
  506. if (!response.ok) {
  507. const error = await response.json();
  508. throw new Error(error.detail || 'Failed to create schedule');
  509. }
  510. alert('SCHEDULE CREATED SUCCESSFULLY');
  511. document.getElementById('agent-id').value = '';
  512. document.getElementById('message').value = '';
  513. document.getElementById('execute-at').value = '';
  514. document.getElementById('cron').value = '';
  515. showTab('schedules');
  516. loadSchedules();
  517. } catch (error) {
  518. errorDiv.textContent = 'Error: ' + error.message;
  519. }
  520. }
  521. async function deleteSchedule(type, id) {
  522. if (!confirm('CONFIRM: Delete this schedule?')) return;
  523. try {
  524. const response = await fetch(`${API_BASE}/schedules/${type}/${id}`, {
  525. method: 'DELETE',
  526. headers: { 'Authorization': `Bearer ${API_KEY}` }
  527. });
  528. if (!response.ok) throw new Error('Failed to delete');
  529. alert('SCHEDULE DELETED');
  530. loadSchedules();
  531. } catch (error) {
  532. alert('ERROR DELETING SCHEDULE: ' + error.message);
  533. }
  534. }
  535. function truncate(str, len) {
  536. return str.length > len ? str.substring(0, len) + '...' : str;
  537. }
  538. </script>
  539. </body>
  540. </html>