| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594 |
- <!DOCTYPE html>
- <html>
- <head>
- <title>Switchboard Dashboard</title>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <style>
- * { box-sizing: border-box; margin: 0; padding: 0; }
- body {
- font-family: 'Courier New', 'Consolas', monospace;
- background: #f5f5dc;
- color: #000;
- padding: 20px;
- line-height: 1.6;
- }
- .container { max-width: 1400px; margin: 0 auto; background: white; padding: 40px; box-shadow: 0 0 20px rgba(0,0,0,0.1); }
- h1 {
- color: #000;
- margin-bottom: 10px;
- font-size: 18px;
- letter-spacing: 3px;
- font-weight: bold;
- text-transform: uppercase;
- }
- h2 {
- color: #000;
- margin: 30px 0 15px;
- font-size: 14px;
- letter-spacing: 2px;
- font-weight: bold;
- text-transform: uppercase;
- border-bottom: 2px solid #000;
- padding-bottom: 5px;
- }
-
- /* API Key Section */
- #api-key-section {
- background: #fafafa;
- border: 2px solid #000;
- padding: 20px;
- margin-bottom: 30px;
- }
- #api-key-section input {
- width: 100%;
- padding: 10px;
- border: 2px solid #666;
- background: white;
- color: #000;
- font-family: 'Courier New', monospace;
- font-size: 14px;
- }
- #api-key-section input:focus {
- outline: none;
- border-color: #000;
- box-shadow: none;
- }
-
- /* Buttons */
- button {
- background: white;
- color: #000;
- border: 2px solid #000;
- padding: 8px 16px;
- cursor: pointer;
- font-size: 12px;
- font-family: 'Courier New', monospace;
- margin-top: 10px;
- text-transform: uppercase;
- letter-spacing: 1px;
- font-weight: bold;
- }
- button:hover {
- background: #000;
- color: white;
- }
- button.danger {
- border-color: #000;
- color: #000;
- }
- button.danger:hover {
- background: #000;
- color: white;
- }
-
- /* Tabs */
- .tabs {
- display: flex;
- gap: 2px;
- margin-bottom: 0;
- border-bottom: 3px solid #000;
- }
- .tab {
- padding: 10px 20px;
- background: #e8e8e8;
- color: #000;
- border: 2px solid #000;
- border-bottom: none;
- cursor: pointer;
- font-weight: bold;
- font-family: 'Courier New', monospace;
- text-transform: uppercase;
- letter-spacing: 1px;
- font-size: 12px;
- }
- .tab:hover { background: #d0d0d0; }
- .tab.active {
- background: white;
- color: #000;
- border-bottom: 3px solid white;
- margin-bottom: -3px;
- }
-
- /* Content */
- .content {
- background: white;
- border: 2px solid #000;
- border-top: none;
- padding: 20px;
- display: none;
- }
- .content.active { display: block; }
-
- /* Tables */
- table {
- width: 100%;
- border-collapse: collapse;
- margin-top: 15px;
- border: 2px solid #000;
- }
- th, td {
- padding: 10px 12px;
- text-align: left;
- border: 1px solid #000;
- }
- th {
- background: #f0f0f0;
- font-weight: bold;
- color: #000;
- text-transform: uppercase;
- font-size: 11px;
- letter-spacing: 1px;
- }
- tr:hover { background: #fafafa; }
- td { font-size: 13px; }
-
- /* Forms */
- .form-group {
- margin-bottom: 20px;
- }
- label {
- display: block;
- margin-bottom: 8px;
- font-weight: bold;
- color: #000;
- text-transform: uppercase;
- font-size: 11px;
- letter-spacing: 1px;
- }
- input[type="text"], input[type="datetime-local"], textarea, select {
- width: 100%;
- padding: 10px;
- border: 2px solid #666;
- background: white;
- color: #000;
- font-family: 'Courier New', monospace;
- font-size: 14px;
- }
- input:focus, textarea:focus, select:focus {
- outline: none;
- border-color: #000;
- box-shadow: none;
- }
- textarea { min-height: 80px; }
- small { color: #666; font-size: 11px; }
-
- /* Status badges */
- .badge {
- padding: 4px 10px;
- border: 2px solid;
- font-size: 11px;
- font-weight: bold;
- font-family: 'Courier New', monospace;
- text-transform: uppercase;
- letter-spacing: 1px;
- }
- .badge.success {
- border-color: #000;
- color: #000;
- background: white;
- }
- .badge.failed {
- border-color: #000;
- color: white;
- background: #000;
- }
-
- .hidden { display: none; }
- .error { color: #000; margin-top: 10px; font-weight: bold; }
- .empty { text-align: center; padding: 40px; color: #666; }
-
- /* Loading spinner */
- .loading {
- text-align: center;
- padding: 40px;
- color: #000;
- }
- .spinner {
- display: inline-block;
- width: 40px;
- height: 40px;
- border: 4px solid #e0e0e0;
- border-top-color: #000;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin-bottom: 10px;
- }
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
- @keyframes blink {
- 0%, 50% { opacity: 1; }
- 51%, 100% { opacity: 0.3; }
- }
- .blink { animation: blink 1s infinite; }
- </style>
- </head>
- <body>
- <div class="container">
- <h1>LETTA SWITCHBOARD — MESSAGE ROUTING SERVICE</h1>
- <div style="border: 2px solid #000; padding: 10px; margin-bottom: 30px; background: #fafafa;">
- <div style="font-size: 11px; line-height: 1.8;">
- <strong>SYSTEM:</strong> MESSAGE ROUTING TERMINAL<br>
- <strong>VERSION:</strong> 1.0.0<br>
- <strong>STATUS:</strong> OPERATIONAL<br>
- <strong>PROTOCOL:</strong> REST API + SCHEDULED EXECUTION
- </div>
- </div>
-
- <!-- API Key Input -->
- <div id="api-key-section">
- <label for="api-key-input">AUTHENTICATION</label>
- <input type="password" id="api-key-input" placeholder="sk-let-...">
- <button onclick="saveApiKey()">SAVE KEY</button>
- <button onclick="clearApiKey()" class="danger">CLEAR KEY</button>
- <div id="api-key-status"></div>
- </div>
-
- <div id="main-content" class="hidden">
- <!-- Tabs -->
- <div class="tabs">
- <button class="tab active" onclick="showTab('schedules')">SCHEDULES</button>
- <button class="tab" onclick="showTab('create')">CREATE NEW</button>
- <button class="tab" onclick="showTab('results')">RESULTS</button>
- </div>
-
- <!-- Schedules Tab -->
- <div id="schedules-tab" class="content active">
- <h2>ONE-TIME SCHEDULES</h2>
- <table id="onetime-table">
- <thead>
- <tr>
- <th>ID</th>
- <th>Agent ID</th>
- <th>Execute At</th>
- <th>Message</th>
- <th>Actions</th>
- </tr>
- </thead>
- <tbody></tbody>
- </table>
-
- <h2>RECURRING SCHEDULES</h2>
- <table id="recurring-table">
- <thead>
- <tr>
- <th>ID</th>
- <th>Agent ID</th>
- <th>Cron</th>
- <th>Message</th>
- <th>Last Run</th>
- <th>Actions</th>
- </tr>
- </thead>
- <tbody></tbody>
- </table>
-
- <button onclick="loadSchedules()" style="margin-top: 20px;">REFRESH</button>
- </div>
-
- <!-- Create Tab -->
- <div id="create-tab" class="content">
- <h2>CREATE SCHEDULE</h2>
-
- <div class="form-group">
- <label>SCHEDULE TYPE</label>
- <select id="schedule-type" onchange="toggleScheduleType()">
- <option value="onetime">One-Time</option>
- <option value="recurring">Recurring</option>
- </select>
- </div>
-
- <div class="form-group">
- <label for="agent-id">AGENT ID</label>
- <input type="text" id="agent-id" placeholder="agent-xxx">
- </div>
-
- <div class="form-group">
- <label for="message">MESSAGE</label>
- <textarea id="message" placeholder="Your message to the agent"></textarea>
- </div>
-
- <div id="onetime-fields">
- <div class="form-group">
- <label for="execute-at">EXECUTE AT</label>
- <input type="datetime-local" id="execute-at">
- </div>
- <div class="form-group">
- <label for="timezone">TIMEZONE</label>
- <select id="timezone">
- <option value="local" selected>Local Browser Time</option>
- <option value="UTC">UTC</option>
- <option value="America/New_York">Eastern (US)</option>
- <option value="America/Chicago">Central (US)</option>
- <option value="America/Denver">Mountain (US)</option>
- <option value="America/Los_Angeles">Pacific (US)</option>
- <option value="Europe/London">London</option>
- <option value="Europe/Paris">Paris</option>
- <option value="Asia/Tokyo">Tokyo</option>
- </select>
- <small style="display:block;margin-top:5px;color:#6b7280;">Time will be converted to UTC for storage</small>
- </div>
- </div>
-
- <div id="recurring-fields" class="hidden">
- <div class="form-group">
- <label for="cron">CRON EXPRESSION</label>
- <input type="text" id="cron" placeholder="0 9 * * 1-5">
- <small style="display:block;margin-top:5px;color:#6b7280;">Examples: "0 9 * * *" (daily at 9am), "*/5 * * * *" (every 5 min)</small>
- </div>
- </div>
-
- <button onclick="createSchedule()">CREATE SCHEDULE</button>
- <div id="create-error" class="error"></div>
- </div>
-
- <!-- Results Tab -->
- <div id="results-tab" class="content">
- <h2>EXECUTION RESULTS</h2>
- <table id="results-table">
- <thead>
- <tr>
- <th>Schedule ID</th>
- <th>Type</th>
- <th>Status</th>
- <th>Agent ID</th>
- <th>Message</th>
- <th>Run ID / Error</th>
- <th>Executed At</th>
- </tr>
- </thead>
- <tbody></tbody>
- </table>
-
- <button onclick="loadResults()" style="margin-top: 20px;">REFRESH</button>
- </div>
- </div>
- </div>
-
- <script>
- const API_BASE = window.location.origin;
- let API_KEY = sessionStorage.getItem('letta_api_key') || '';
-
- // Initialize
- if (API_KEY) {
- document.getElementById('api-key-input').value = API_KEY;
- document.getElementById('main-content').classList.remove('hidden');
- loadSchedules();
- loadResults();
- }
-
- function saveApiKey() {
- API_KEY = document.getElementById('api-key-input').value.trim();
- if (!API_KEY) {
- alert('ERROR: API KEY REQUIRED');
- return;
- }
- sessionStorage.setItem('letta_api_key', API_KEY);
- document.getElementById('main-content').classList.remove('hidden');
- document.getElementById('api-key-status').innerHTML = '<p style="color:#000;margin-top:10px;font-weight:bold;">✓ API KEY AUTHENTICATED</p>';
- loadSchedules();
- loadResults();
- }
-
- function clearApiKey() {
- sessionStorage.removeItem('letta_api_key');
- API_KEY = '';
- document.getElementById('api-key-input').value = '';
- document.getElementById('main-content').classList.add('hidden');
- document.getElementById('api-key-status').innerHTML = '<p style="color:#000;margin-top:10px;font-weight:bold;">✗ SESSION TERMINATED</p>';
- }
-
- function showTab(tab) {
- document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
- document.querySelectorAll('.content').forEach(c => c.classList.remove('active'));
- event.target.classList.add('active');
- document.getElementById(`${tab}-tab`).classList.add('active');
- }
-
- function toggleScheduleType() {
- const type = document.getElementById('schedule-type').value;
- document.getElementById('onetime-fields').classList.toggle('hidden', type !== 'onetime');
- document.getElementById('recurring-fields').classList.toggle('hidden', type !== 'recurring');
- }
-
- async function loadSchedules() {
- const onetimeBody = document.querySelector('#onetime-table tbody');
- const recurringBody = document.querySelector('#recurring-table tbody');
-
- // Show loading spinners
- onetimeBody.innerHTML = '<tr><td colspan="5" class="loading"><div class="spinner"></div><p>Loading...</p></td></tr>';
- recurringBody.innerHTML = '<tr><td colspan="6" class="loading"><div class="spinner"></div><p>Loading...</p></td></tr>';
-
- try {
- const [onetime, recurring] = await Promise.all([
- fetch(`${API_BASE}/schedules/one-time`, {
- headers: { 'Authorization': `Bearer ${API_KEY}` }
- }).then(r => r.json()),
- fetch(`${API_BASE}/schedules/recurring`, {
- headers: { 'Authorization': `Bearer ${API_KEY}` }
- }).then(r => r.json())
- ]);
-
- // One-time schedules
- onetimeBody.innerHTML = onetime.length ? onetime.map(s => `
- <tr>
- <td>${s.id.substring(0, 8)}...</td>
- <td>${s.agent_id}</td>
- <td>${new Date(s.execute_at).toLocaleString()}</td>
- <td>${truncate(s.message, 50)}</td>
- <td><button class="danger" onclick="deleteSchedule('one-time', '${s.id}')">DEL</button></td>
- </tr>
- `).join('') : '<tr><td colspan="5" class="empty">No one-time schedules</td></tr>';
-
- // Recurring schedules
- const recurringBody = document.querySelector('#recurring-table tbody');
- recurringBody.innerHTML = recurring.length ? recurring.map(s => `
- <tr>
- <td>${s.id.substring(0, 8)}...</td>
- <td>${s.agent_id}</td>
- <td>${s.cron}</td>
- <td>${truncate(s.message, 50)}</td>
- <td>${s.last_run ? new Date(s.last_run).toLocaleString() : 'Never'}</td>
- <td><button class="danger" onclick="deleteSchedule('recurring', '${s.id}')">DEL</button></td>
- </tr>
- `).join('') : '<tr><td colspan="6" class="empty">No recurring schedules</td></tr>';
- } catch (error) {
- alert('ERROR LOADING SCHEDULES: ' + error.message);
- }
- }
-
- async function loadResults() {
- const resultsBody = document.querySelector('#results-table tbody');
-
- // Show loading spinner
- resultsBody.innerHTML = '<tr><td colspan="7" class="loading"><div class="spinner"></div><p>Loading...</p></td></tr>';
-
- try {
- const results = await fetch(`${API_BASE}/results`, {
- headers: { 'Authorization': `Bearer ${API_KEY}` }
- }).then(r => r.json());
-
- resultsBody.innerHTML = results.length ? results.map(r => `
- <tr>
- <td>${r.schedule_id.substring(0, 8)}...</td>
- <td>${r.schedule_type}</td>
- <td><span class="badge ${r.status}">${r.status}</span></td>
- <td>${r.agent_id}</td>
- <td>${truncate(r.message, 30)}</td>
- <td>${r.run_id || r.error || 'N/A'}</td>
- <td>${new Date(r.executed_at).toLocaleString()}</td>
- </tr>
- `).join('') : '<tr><td colspan="7" class="empty">No execution results</td></tr>';
- } catch (error) {
- alert('ERROR LOADING RESULTS: ' + error.message);
- }
- }
-
- async function createSchedule() {
- const type = document.getElementById('schedule-type').value;
- const agentId = document.getElementById('agent-id').value.trim();
- const message = document.getElementById('message').value.trim();
- const errorDiv = document.getElementById('create-error');
-
- errorDiv.textContent = '';
-
- if (!agentId || !message) {
- errorDiv.textContent = 'Please fill in all fields';
- return;
- }
-
- try {
- const payload = {
- agent_id: agentId,
- message: message,
- role: 'user'
- };
-
- if (type === 'onetime') {
- const executeAt = document.getElementById('execute-at').value;
- const timezone = document.getElementById('timezone').value;
-
- if (!executeAt) {
- errorDiv.textContent = 'Please specify execution time';
- return;
- }
-
- // Convert datetime-local to ISO 8601 with timezone handling
- let executeDate;
- if (timezone === 'local') {
- // Use local browser timezone
- executeDate = new Date(executeAt);
- } else if (timezone === 'UTC') {
- // Treat input as UTC
- executeDate = new Date(executeAt + 'Z');
- } else {
- // For named timezones, we assume the datetime-local is in that timezone
- // Convert to UTC by parsing as local and adjusting
- executeDate = new Date(executeAt);
- // Note: This is a simplification. For accurate timezone conversion,
- // we'd need a library, but for now we assume UTC or local
- }
-
- payload.execute_at = executeDate.toISOString();
- } else {
- const cron = document.getElementById('cron').value.trim();
- if (!cron) {
- errorDiv.textContent = 'Please specify cron expression';
- return;
- }
- payload.cron = cron;
- }
-
- const endpoint = type === 'onetime' ? '/schedules/one-time' : '/schedules/recurring';
- const response = await fetch(`${API_BASE}${endpoint}`, {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${API_KEY}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(payload)
- });
-
- if (!response.ok) {
- const error = await response.json();
- throw new Error(error.detail || 'Failed to create schedule');
- }
-
- alert('SCHEDULE CREATED SUCCESSFULLY');
- document.getElementById('agent-id').value = '';
- document.getElementById('message').value = '';
- document.getElementById('execute-at').value = '';
- document.getElementById('cron').value = '';
-
- showTab('schedules');
- loadSchedules();
- } catch (error) {
- errorDiv.textContent = 'Error: ' + error.message;
- }
- }
-
- async function deleteSchedule(type, id) {
- if (!confirm('CONFIRM: Delete this schedule?')) return;
-
- try {
- const response = await fetch(`${API_BASE}/schedules/${type}/${id}`, {
- method: 'DELETE',
- headers: { 'Authorization': `Bearer ${API_KEY}` }
- });
-
- if (!response.ok) throw new Error('Failed to delete');
-
- alert('SCHEDULE DELETED');
- loadSchedules();
- } catch (error) {
- alert('ERROR DELETING SCHEDULE: ' + error.message);
- }
- }
-
- function truncate(str, len) {
- return str.length > len ? str.substring(0, len) + '...' : str;
- }
- </script>
- </body>
- </html>
|