This commit is contained in:
pavel 2026-02-10 20:33:04 +01:00
commit 643203b8a5
9 changed files with 822 additions and 44 deletions

View file

@ -2,6 +2,13 @@ import { marked } from 'marked';
import DOMPurify from 'dompurify';
const API_URL = 'http://localhost:3000';
// These should ideally be environment-specific
const AUTH_CONFIG = {
issuer: 'https://idm.flegr.me/application/o/bot/',
clientId: 'CicDk8mpSBY1SW4ofCamO3B583ttmvKTSPDQwvpb',
redirectUri: window.location.origin + '/callback',
authorizeEndpoint: 'https://idm.flegr.me/application/o/authorize/',
};
const state = {
tasks: [],
@ -9,9 +16,15 @@ const state = {
selectedRunId: null,
currentView: 'dashboard', // 'dashboard' or 'task'
isEditing: false,
token: localStorage.getItem('auth_token')
};
// DOM elements
const loginOverlay = document.getElementById('login-overlay');
const callbackOverlay = document.getElementById('callback-overlay');
const loginBtn = document.getElementById('login-btn');
const appEl = document.getElementById('app');
const logoutBtn = document.getElementById('logout-btn');
const taskListEl = document.getElementById('task-list');
const newTaskBtn = document.getElementById('new-task-btn');
const modalContainer = document.getElementById('modal-container');
@ -42,9 +55,31 @@ const toggleCustomCronBtn = document.getElementById('toggle-custom-cron');
const customCronContainer = document.getElementById('custom-cron-container');
const presetBtns = document.querySelectorAll('.btn-preset');
// Wrapper for fetch to include Authorization header
async function fetchWithAuth(url, options = {}) {
if (!state.token) {
showLogin();
throw new Error('Not authenticated');
}
const headers = {
...options.headers,
'Authorization': `Bearer ${state.token}`
};
const response = await fetch(url, { ...options, headers });
if (response.status === 401) {
logout();
throw new Error('Session expired');
}
return response;
}
async function fetchTasks() {
try {
const response = await fetch(`${API_URL}/tasks`);
const response = await fetchWithAuth(`${API_URL}/tasks`);
const newTasks = await response.json();
// Check if we should follow the latest run (if we were already watching it)
@ -88,7 +123,7 @@ async function fetchTasks() {
async function fetchRecentRuns() {
try {
const response = await fetch(`${API_URL}/runs/recent`);
const response = await fetchWithAuth(`${API_URL}/runs/recent`);
const recentRuns = await response.json();
renderDashboard(recentRuns);
} catch (error) {
@ -230,8 +265,11 @@ function showTaskView(task) {
if (!run) {
viewStatusEl.textContent = 'No runs';
logsOutputEl.innerHTML = '';
answerContainerEl.classList.add('hidden');
viewStatusEl.className = 'status-badge pending';
viewDateEl.textContent = '-';
logsOutputEl.innerHTML = '<div class="waiting-placeholder">No logs available. Click "Run Task" to start the agent.</div>';
answerContainerEl.classList.remove('hidden');
answerOutputEl.innerHTML = '<div class="waiting-placeholder">Waiting for the first execution...</div>';
return;
}
@ -245,12 +283,12 @@ function showTaskView(task) {
minute: '2-digit'
});
answerContainerEl.classList.remove('hidden');
if (run.answer) {
answerContainerEl.classList.remove('hidden');
const rawHtml = marked.parse(run.answer);
answerOutputEl.innerHTML = DOMPurify.sanitize(rawHtml);
} else {
answerContainerEl.classList.add('hidden');
answerOutputEl.innerHTML = '<div class="waiting-placeholder">Agent is working on the final answer...</div>';
}
// Check if we should auto-scroll
@ -279,7 +317,7 @@ rerunBtn.addEventListener('click', async () => {
if (!state.selectedTaskId) return;
try {
const response = await fetch(`${API_URL}/tasks/${state.selectedTaskId}/runs`, {
const response = await fetchWithAuth(`${API_URL}/tasks/${state.selectedTaskId}/runs`, {
method: 'POST',
});
const updatedTask = await response.json();
@ -289,15 +327,15 @@ rerunBtn.addEventListener('click', async () => {
}
selectTask(updatedTask.id);
} catch (error) {
console.error('Error re-running task:', error);
alert('Failed to re-run task.');
console.error('Error running task:', error);
alert('Failed to run task.');
}
});
newTaskBtn.addEventListener('click', () => {
state.isEditing = false;
modalTitle.textContent = 'New Agent Task';
submitTaskBtn.textContent = 'Execute Directive';
submitTaskBtn.textContent = 'Save Task';
goalTextarea.value = '';
cronInput.value = '';
updateScheduleUI('');
@ -316,8 +354,8 @@ editTaskBtn.addEventListener('click', () => {
if (!task) return;
state.isEditing = true;
modalTitle.textContent = 'Edit Directive';
submitTaskBtn.textContent = 'Update Directive';
modalTitle.textContent = 'Edit Agent Task';
submitTaskBtn.textContent = 'Save Task';
goalTextarea.value = task.goal;
cronInput.value = task.cron || '';
updateScheduleUI(task.cron || '');
@ -383,13 +421,13 @@ newTaskForm.addEventListener('submit', async (e) => {
try {
let response;
if (state.isEditing) {
response = await fetch(`${API_URL}/tasks/${state.selectedTaskId}`, {
response = await fetchWithAuth(`${API_URL}/tasks/${state.selectedTaskId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ goal, cron }),
});
} else {
response = await fetch(`${API_URL}/tasks`, {
response = await fetchWithAuth(`${API_URL}/tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ goal, cron }),
@ -415,11 +453,6 @@ newTaskForm.addEventListener('submit', async (e) => {
}
});
// Initial load
// Initial fetch
fetchTasks();
// Auto-refresh every 3 seconds
let isPolling = false;
async function startAutoRefresh() {
setInterval(async () => {
@ -433,4 +466,71 @@ async function startAutoRefresh() {
}, 3000);
}
startAutoRefresh();
async function showLogin() {
loginOverlay.classList.remove('hidden');
appEl.classList.add('hidden');
}
async function logout() {
state.token = null;
localStorage.removeItem('auth_token');
showLogin();
}
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
if (!code) return;
window.history.replaceState({}, document.title, "/");
callbackOverlay.classList.remove('hidden');
loginOverlay.classList.add('hidden');
try {
const response = await fetch(`${API_URL}/auth/callback?code=${code}&redirect_uri=${encodeURIComponent(AUTH_CONFIG.redirectUri)}`);
const data = await response.json();
if (data.access_token) {
state.token = data.access_token;
localStorage.setItem('auth_token', data.access_token);
callbackOverlay.classList.add('hidden');
appEl.classList.remove('hidden');
initializeApp();
} else {
throw new Error('No access token in response');
}
} catch (error) {
console.error('Auth callback failed:', error);
alert('Authentication failed.');
showLogin();
}
}
loginBtn.addEventListener('click', () => {
const authUrl = `${AUTH_CONFIG.authorizeEndpoint}?client_id=${AUTH_CONFIG.clientId}&response_type=code&redirect_uri=${encodeURIComponent(AUTH_CONFIG.redirectUri)}&scope=openid profile email`;
window.location.href = authUrl;
});
logoutBtn.addEventListener('click', () => {
logout();
});
async function initializeApp() {
if (!state.token) {
showLogin();
return;
}
appEl.classList.remove('hidden');
loginOverlay.classList.add('hidden');
await fetchTasks();
startAutoRefresh();
}
// Check for callback on load
if (window.location.pathname === '/callback' || window.location.search.includes('code=')) {
handleCallback();
} else {
initializeApp();
}

View file

@ -434,6 +434,21 @@ body {
padding-bottom: 8px;
}
.answer-output {
min-height: 100px;
}
.waiting-placeholder {
color: var(--text-dim);
font-style: italic;
display: flex;
align-items: center;
justify-content: center;
height: 100px;
background: rgba(255, 255, 255, 0.02);
border-radius: 8px;
}
.answer-output h2 {
font-size: 20px;
}
@ -711,4 +726,91 @@ textarea:focus {
::-webkit-scrollbar-thumb:hover {
background: var(--text-dim);
}
.login-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
background: rgba(10, 10, 15, 0.8);
backdrop-filter: blur(20px);
}
.login-box {
text-align: center;
max-width: 400px;
width: 90%;
padding: 40px;
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
align-items: center;
}
.login-box .logo-icon {
font-size: 64px;
margin-bottom: 16px;
display: block;
}
.login-title {
font-size: 32px;
font-weight: 700;
margin-bottom: 8px;
letter-spacing: -0.02em;
}
.login-box p {
color: var(--text-dim);
margin-bottom: 32px;
}
.loader {
width: 48px;
height: 48px;
border: 4px solid rgba(255, 255, 255, 0.1);
border-left-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.sidebar-footer {
padding: 16px;
border-top: 1px solid var(--glass-border);
margin-top: auto;
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
.logout-btn {
/* Styles for logout button, based on common patterns */
background: transparent;
color: var(--text-dim);
border: none;
padding: 8px 12px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: color 0.2s ease;
}
.logout-btn:hover {
color: var(--primary);
}