fix: restore task and agent ui rendering
This commit is contained in:
740
public/app.js
740
public/app.js
@@ -68,112 +68,6 @@ const COLUMNS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let wikiPages = [];
|
let wikiPages = [];
|
||||||
let allTasks = [];
|
|
||||||
let taskFilters = { search: '', status: '', assignee: '', priority: '' };
|
|
||||||
|
|
||||||
async function loadTasks() {
|
|
||||||
const res = await fetch('/api/tasks');
|
|
||||||
allTasks = await res.json();
|
|
||||||
|
|
||||||
// Populate assignee filter dropdown
|
|
||||||
const assigneeFilter = document.getElementById('filter-assignee');
|
|
||||||
if (assigneeFilter) {
|
|
||||||
const assignees = [...new Set(allTasks.map(t => t.assignee).filter(Boolean))];
|
|
||||||
assigneeFilter.innerHTML = '<option value="">All Assignees</option>' +
|
|
||||||
assignees.map(a => `<option value="${a}">${a}</option>`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
applyTaskFilters();
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyTaskFilters() {
|
|
||||||
let filtered = [...allTasks];
|
|
||||||
|
|
||||||
// Search filter
|
|
||||||
if (taskFilters.search) {
|
|
||||||
const search = taskFilters.search.toLowerCase();
|
|
||||||
filtered = filtered.filter(t =>
|
|
||||||
t.title.toLowerCase().includes(search) ||
|
|
||||||
(t.description && t.description.toLowerCase().includes(search))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status filter
|
|
||||||
if (taskFilters.status) {
|
|
||||||
filtered = filtered.filter(t => t.status === taskFilters.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assignee filter
|
|
||||||
if (taskFilters.assignee) {
|
|
||||||
filtered = filtered.filter(t => t.assignee === taskFilters.assignee);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority filter
|
|
||||||
if (taskFilters.priority) {
|
|
||||||
filtered = filtered.filter(t => t.priority === taskFilters.priority);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset columns
|
|
||||||
Object.keys(COLUMNS).forEach((status) => {
|
|
||||||
COLUMNS[status].tasks = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
// Distribute filtered tasks to columns
|
|
||||||
filtered.forEach((task) => {
|
|
||||||
if (COLUMNS[task.status]) {
|
|
||||||
COLUMNS[task.status].tasks.push(task);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
renderBoard();
|
|
||||||
}
|
|
||||||
|
|
||||||
function initTaskFilters() {
|
|
||||||
const searchInput = document.getElementById('task-search');
|
|
||||||
const statusFilter = document.getElementById('filter-status');
|
|
||||||
const assigneeFilter = document.getElementById('filter-assignee');
|
|
||||||
const priorityFilter = document.getElementById('filter-priority');
|
|
||||||
const clearBtn = document.getElementById('clear-filters');
|
|
||||||
|
|
||||||
if (searchInput) {
|
|
||||||
searchInput.addEventListener('input', (e) => {
|
|
||||||
taskFilters.search = e.target.value;
|
|
||||||
applyTaskFilters();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusFilter) {
|
|
||||||
statusFilter.addEventListener('change', (e) => {
|
|
||||||
taskFilters.status = e.target.value;
|
|
||||||
applyTaskFilters();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (assigneeFilter) {
|
|
||||||
assigneeFilter.addEventListener('change', (e) => {
|
|
||||||
taskFilters.assignee = e.target.value;
|
|
||||||
applyTaskFilters();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (priorityFilter) {
|
|
||||||
priorityFilter.addEventListener('change', (e) => {
|
|
||||||
taskFilters.priority = e.target.value;
|
|
||||||
applyTaskFilters();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clearBtn) {
|
|
||||||
clearBtn.addEventListener('click', () => {
|
|
||||||
taskFilters = { search: '', status: '', assignee: '', priority: '' };
|
|
||||||
if (searchInput) searchInput.value = '';
|
|
||||||
if (statusFilter) statusFilter.value = '';
|
|
||||||
if (assigneeFilter) assigneeFilter.value = '';
|
|
||||||
if (priorityFilter) priorityFilter.value = '';
|
|
||||||
applyTaskFilters();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let currentWikiPage = null;
|
let currentWikiPage = null;
|
||||||
let allAgents = [];
|
let allAgents = [];
|
||||||
let usageStats = null;
|
let usageStats = null;
|
||||||
@@ -222,41 +116,13 @@ function renderBoard() {
|
|||||||
const cardEl = document.createElement('div');
|
const cardEl = document.createElement('div');
|
||||||
cardEl.className = 'card';
|
cardEl.className = 'card';
|
||||||
cardEl.innerHTML = `
|
cardEl.innerHTML = `
|
||||||
<div class="agent-header">
|
<div class="card-head">
|
||||||
<h3 class="agent-name">${agent.emoji || '🤖'} ${escapeHtml(agent.name)}</h3>
|
<h3 class="card-title">${escapeHtml(task.title)}</h3>
|
||||||
<span class="agent-status ${statusClass}">${agent.status}</span>
|
<span class="badge priority-${task.priority}">${task.priority}</span>
|
||||||
</div>
|
|
||||||
<div class="agent-meta">
|
|
||||||
<span class="agent-model">🧠 ${agent.model || 'unknown'}</span>
|
|
||||||
<span class="agent-activity">⏰ ${agent.lastActivity ? new Date(agent.lastActivity).toLocaleString() : 'Never'}</span>
|
|
||||||
</div>
|
|
||||||
<div class="agent-workload">
|
|
||||||
<span class="workload-badge">📋 ${agent.workload} active task${agent.workload !== 1 ? 's' : ''}</span>
|
|
||||||
</div>
|
|
||||||
<div class="agent-body">
|
|
||||||
<div class="agent-section">
|
|
||||||
<h4>📋 Current Task</h4>
|
|
||||||
<p class="agent-task">${agent.currentTask || 'No active task'}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="agent-section">
|
<p class="card-desc">${escapeHtml(task.description || '')}</p>
|
||||||
<h4>🛠️ Tools</h4>
|
<p class="meta assignee">${task.assignee || 'Unassigned'}</p>
|
||||||
<div class="agent-tools">
|
<p class="meta tags">${task.tags.map((t) => `<span class="tag">${escapeHtml(t)}</span>`).join(' ')}</p>
|
||||||
${agent.tools.length ? agent.tools.slice(0, 5).map((tool) => \`<span class="tool-tag">\${escapeHtml(tool)}</span>\`).join('') : '<span class="no-data">No tools</span>'}
|
|
||||||
${agent.tools.length > 5 ? \`<span class="more-tag">+\${agent.tools.length - 5} more</span>\` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="agent-section">
|
|
||||||
<h4>📄 Recent Files</h4>
|
|
||||||
<div class="agent-files">
|
|
||||||
${agent.files.length ? agent.files.slice(0, 5).map((file) => \`<span class="file-tag">\${escapeHtml(file)}</span>\`).join('') : '<span class="no-data">No files</span>'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="agent-actions">
|
|
||||||
<button class="btn-secondary agent-details-btn" data-agent="\${escapeHtml(agent.name)}">Details</button>
|
|
||||||
<button class="btn-primary agent-assign-btn" data-agent="\${escapeHtml(agent.name)}">Assign Task</button>
|
|
||||||
</div>
|
|
||||||
`<span class="tag">${escapeHtml(t)}</span>`).join(' ')}</p>
|
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" class="card-check" ${task.status === 'Done' ? 'checked' : ''} />
|
<input type="checkbox" class="card-check" ${task.status === 'Done' ? 'checked' : ''} />
|
||||||
Mark Complete
|
Mark Complete
|
||||||
@@ -270,8 +136,7 @@ function renderBoard() {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ status: 'Done' }),
|
body: JSON.stringify({ status: 'Done' }),
|
||||||
});
|
});
|
||||||
initDragAndDrop();
|
loadTasks();
|
||||||
loadTasks();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cardsEl.appendChild(cardEl);
|
cardsEl.appendChild(cardEl);
|
||||||
@@ -329,13 +194,10 @@ function initTasksPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
e.target.reset();
|
e.target.reset();
|
||||||
initDragAndDrop();
|
loadTasks();
|
||||||
loadTasks();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
initTaskFilters();
|
|
||||||
populateAgentDropdown();
|
populateAgentDropdown();
|
||||||
initDragAndDrop();
|
|
||||||
loadTasks();
|
loadTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,13 +419,9 @@ function renderAgents(filter = '', statusFilter = '') {
|
|||||||
|
|
||||||
cardEl.innerHTML = `
|
cardEl.innerHTML = `
|
||||||
<div class="agent-header">
|
<div class="agent-header">
|
||||||
<h3 class="agent-name">${agent.emoji || '🤖'} ${escapeHtml(agent.name)}</h3>
|
<h3 class="agent-name">${escapeHtml(agent.name)}</h3>
|
||||||
<span class="agent-status ${statusClass}">${agent.status}</span>
|
<span class="agent-status ${statusClass}">${agent.status}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="agent-meta">
|
|
||||||
<span class="agent-model">🧠 ${agent.model || 'unknown'}</span>
|
|
||||||
<span class="agent-activity">⏰ ${agent.lastActivity ? new Date(agent.lastActivity).toLocaleString() : 'Never'}</span>
|
|
||||||
</div>
|
|
||||||
<div class="agent-workload">
|
<div class="agent-workload">
|
||||||
<span class="workload-badge">📋 ${agent.workload} active task${agent.workload !== 1 ? 's' : ''}</span>
|
<span class="workload-badge">📋 ${agent.workload} active task${agent.workload !== 1 ? 's' : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -575,22 +433,7 @@ function renderAgents(filter = '', statusFilter = '') {
|
|||||||
<div class="agent-section">
|
<div class="agent-section">
|
||||||
<h4>🛠️ Tools</h4>
|
<h4>🛠️ Tools</h4>
|
||||||
<div class="agent-tools">
|
<div class="agent-tools">
|
||||||
${agent.tools.length ? agent.tools.slice(0, 5).map((tool) => \`<span class="tool-tag">\${escapeHtml(tool)}</span>\`).join('') : '<span class="no-data">No tools</span>'}
|
${agent.tools.length ? agent.tools.slice(0, 5).map((tool) => `<span class="tool-tag">${escapeHtml(tool)}</span>`).join('') : '<span class="no-data">No tools</span>'}
|
||||||
${agent.tools.length > 5 ? \`<span class="more-tag">+\${agent.tools.length - 5} more</span>\` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="agent-section">
|
|
||||||
<h4>📄 Recent Files</h4>
|
|
||||||
<div class="agent-files">
|
|
||||||
${agent.files.length ? agent.files.slice(0, 5).map((file) => \`<span class="file-tag">\${escapeHtml(file)}</span>\`).join('') : '<span class="no-data">No files</span>'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="agent-actions">
|
|
||||||
<button class="btn-secondary agent-details-btn" data-agent="\${escapeHtml(agent.name)}">Details</button>
|
|
||||||
<button class="btn-primary agent-assign-btn" data-agent="\${escapeHtml(agent.name)}">Assign Task</button>
|
|
||||||
</div>
|
|
||||||
`<span class="tool-tag">${escapeHtml(tool)}</span>`).join('') : '<span class="no-data">No tools</span>'}
|
|
||||||
${agent.tools.length > 5 ? `<span class="more-tag">+${agent.tools.length - 5} more</span>` : ''}
|
${agent.tools.length > 5 ? `<span class="more-tag">+${agent.tools.length - 5} more</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -754,35 +597,6 @@ function initAgentsPage() {
|
|||||||
|
|
||||||
// ============ USAGE ============
|
// ============ USAGE ============
|
||||||
async function loadUsage() {
|
async function loadUsage() {
|
||||||
// Try real usage first, fallback to tracked usage
|
|
||||||
try {
|
|
||||||
const from = document.getElementById("usage-from")?.value;
|
|
||||||
const to = document.getElementById("usage-to")?.value;
|
|
||||||
let url = "/api/usage/real";
|
|
||||||
const params = [];
|
|
||||||
if (from) params.push(`from=${from}`);
|
|
||||||
if (to) params.push(`to=${to}`);
|
|
||||||
if (params.length) url += `?${params.join("&")}`;
|
|
||||||
const realRes = await fetch(url);
|
|
||||||
if (realRes.ok) {
|
|
||||||
const realData = await realRes.json();
|
|
||||||
usageStats = {
|
|
||||||
totalRequests: Object.values(realData.models || {}).reduce((sum, m) => sum + (m.requests || 0), 0),
|
|
||||||
totalTokens: realData.totals?.total || 0,
|
|
||||||
totalCost: realData.totals?.cost || 0,
|
|
||||||
agents: realData.agents,
|
|
||||||
models: realData.models
|
|
||||||
};
|
|
||||||
renderUsageStats();
|
|
||||||
if (typeof renderRealUsageCharts === 'function') {
|
|
||||||
renderRealUsageCharts(realData);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Real usage not available, falling back");
|
|
||||||
}
|
|
||||||
|
|
||||||
const from = document.getElementById('usage-from')?.value;
|
const from = document.getElementById('usage-from')?.value;
|
||||||
const to = document.getElementById('usage-to')?.value;
|
const to = document.getElementById('usage-to')?.value;
|
||||||
|
|
||||||
@@ -807,47 +621,7 @@ async function loadUsage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRealUsageCharts(data) {
|
function renderUsageStats() {
|
||||||
if (typeof Chart === 'undefined') return;
|
|
||||||
|
|
||||||
// Agent chart
|
|
||||||
const agentCanvas = document.getElementById('chart-agent');
|
|
||||||
if (agentCanvas && data.agents) {
|
|
||||||
const agents = Object.entries(data.agents)
|
|
||||||
.filter(([_, v]) => v.total > 0)
|
|
||||||
.sort((a, b) => b[1].total - a[1].total)
|
|
||||||
.slice(0, 10);
|
|
||||||
|
|
||||||
new Chart(agentCanvas, {
|
|
||||||
type: 'bar',
|
|
||||||
data: {
|
|
||||||
labels: agents.map(([k]) => k),
|
|
||||||
datasets: [{
|
|
||||||
label: 'Tokens',
|
|
||||||
data: agents.map(([_, v]) => v.total),
|
|
||||||
backgroundColor: 'rgba(54, 162, 235, 0.6)'
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
indexAxis: 'y'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provider grid
|
|
||||||
const grid = document.getElementById('provider-grid');
|
|
||||||
if (grid && data.models) {
|
|
||||||
grid.innerHTML = Object.entries(data.models)
|
|
||||||
.map(([model, stats]) => `
|
|
||||||
<div class="provider-card">
|
|
||||||
<h4>${model}</h4>
|
|
||||||
<p>Requests: ${stats.requests || 0}</p>
|
|
||||||
<p>Tokens: ${(stats.input || 0) + (stats.output || 0)}</p>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const requestsEl = document.getElementById('stat-requests');
|
const requestsEl = document.getElementById('stat-requests');
|
||||||
const tokensEl = document.getElementById('stat-tokens');
|
const tokensEl = document.getElementById('stat-tokens');
|
||||||
const costEl = document.getElementById('stat-cost');
|
const costEl = document.getElementById('stat-cost');
|
||||||
@@ -993,7 +767,7 @@ function initUsagePage() {
|
|||||||
exportJsonBtn.addEventListener('click', () => {
|
exportJsonBtn.addEventListener('click', () => {
|
||||||
const fromValue = fromInput.value;
|
const fromValue = fromInput.value;
|
||||||
const toValue = toInput.value;
|
const toValue = toInput.value;
|
||||||
let url = '/api/usage/export/real?format=json';
|
let url = '/api/usage/export?format=json';
|
||||||
if (fromValue) url += `&from=${fromValue}`;
|
if (fromValue) url += `&from=${fromValue}`;
|
||||||
if (toValue) url += `&to=${toValue}`;
|
if (toValue) url += `&to=${toValue}`;
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
@@ -1002,7 +776,7 @@ function initUsagePage() {
|
|||||||
exportCsvBtn.addEventListener('click', () => {
|
exportCsvBtn.addEventListener('click', () => {
|
||||||
const fromValue = fromInput.value;
|
const fromValue = fromInput.value;
|
||||||
const toValue = toInput.value;
|
const toValue = toInput.value;
|
||||||
let url = '/api/usage/export/real?format=csv';
|
let url = '/api/usage/export?format=csv';
|
||||||
if (fromValue) url += `&from=${fromValue}`;
|
if (fromValue) url += `&from=${fromValue}`;
|
||||||
if (toValue) url += `&to=${toValue}`;
|
if (toValue) url += `&to=${toValue}`;
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
@@ -1044,492 +818,4 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
if (CURRENT_PAGE === 'wiki') initWikiPage();
|
if (CURRENT_PAGE === 'wiki') initWikiPage();
|
||||||
if (CURRENT_PAGE === 'agents') initAgentsPage();
|
if (CURRENT_PAGE === 'agents') initAgentsPage();
|
||||||
if (CURRENT_PAGE === 'usage') initUsagePage();
|
if (CURRENT_PAGE === 'usage') initUsagePage();
|
||||||
if (CURRENT_PAGE === 'gitea') initGiteaPage();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ GITEA DASHBOARD ============
|
|
||||||
let giteaData = {
|
|
||||||
repos: [],
|
|
||||||
reviews: [],
|
|
||||||
activity: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tab switching
|
|
||||||
document.querySelectorAll('.gitea-tabs .tab-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
// Update active tab button
|
|
||||||
document.querySelectorAll('.gitea-tabs .tab-btn').forEach(b => b.classList.remove('active'));
|
|
||||||
btn.classList.add('active');
|
|
||||||
|
|
||||||
// Show corresponding content
|
|
||||||
const tabName = btn.dataset.tab;
|
|
||||||
document.querySelectorAll('.tab-content').forEach(content => {
|
|
||||||
content.classList.remove('active');
|
|
||||||
});
|
|
||||||
document.getElementById(`${tabName}-tab`).classList.add('active');
|
|
||||||
|
|
||||||
// Load data for tab
|
|
||||||
if (tabName === 'swarm') loadGiteaSwarm();
|
|
||||||
else if (tabName === 'reviews') loadGiteaReviews();
|
|
||||||
else if (tabName === 'activity') loadGiteaActivity();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadGiteaSwarm() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/gitea/swarm');
|
|
||||||
const repos = await response.json();
|
|
||||||
giteaData.repos = repos;
|
|
||||||
|
|
||||||
// Update stats
|
|
||||||
const totalRepos = repos.length;
|
|
||||||
const totalPRs = repos.reduce((sum, r) => sum + r.open_prs, 0);
|
|
||||||
const totalIssues = repos.reduce((sum, r) => sum + r.open_issues, 0);
|
|
||||||
const totalBranches = repos.reduce((sum, r) => sum + r.branches, 0);
|
|
||||||
|
|
||||||
document.getElementById('total-repos').textContent = totalRepos;
|
|
||||||
document.getElementById('total-prs').textContent = totalPRs;
|
|
||||||
document.getElementById('total-issues').textContent = totalIssues;
|
|
||||||
document.getElementById('total-branches').textContent = totalBranches;
|
|
||||||
|
|
||||||
// Render repo list
|
|
||||||
const repoList = document.getElementById('repo-list');
|
|
||||||
if (repos.length === 0) {
|
|
||||||
repoList.innerHTML = '<p class="empty">No repositories found</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
repoList.innerHTML = repos.map(repo => `
|
|
||||||
<div class="repo-card">
|
|
||||||
<div class="repo-header">
|
|
||||||
<h3><a href="${repo.html_url}" target="_blank">${repo.name}</a></h3>
|
|
||||||
<span class="repo-stats">
|
|
||||||
⭐ ${repo.stars} 🍴 ${repo.forks}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="repo-metrics">
|
|
||||||
<span class="metric ${repo.open_prs > 0 ? 'has-items' : ''}">
|
|
||||||
🔀 ${repo.open_prs} PRs
|
|
||||||
</span>
|
|
||||||
<span class="metric ${repo.open_issues > 0 ? 'has-items' : ''}">
|
|
||||||
🐛 ${repo.open_issues} Issues
|
|
||||||
</span>
|
|
||||||
<span class="metric">
|
|
||||||
🌿 ${repo.branches} Branches
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="repo-footer">
|
|
||||||
<span class="updated">Updated: ${new Date(repo.updated_at).toLocaleDateString()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading Gitea swarm:', error);
|
|
||||||
document.getElementById('repo-list').innerHTML =
|
|
||||||
`<p class="error">Failed to load repositories: ${error.message}</p>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadGiteaReviews() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/gitea/reviews');
|
|
||||||
const reviews = await response.json();
|
|
||||||
giteaData.reviews = reviews;
|
|
||||||
|
|
||||||
const reviewsList = document.getElementById('reviews-list');
|
|
||||||
if (reviews.length === 0) {
|
|
||||||
reviewsList.innerHTML = '<p class="empty">No pending reviews</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
reviewsList.innerHTML = reviews.map(pr => `
|
|
||||||
<div class="pr-card ${pr.draft ? 'draft' : ''} ${!pr.mergeable ? 'conflict' : ''}">
|
|
||||||
<div class="pr-header">
|
|
||||||
<span class="pr-repo">${pr.repo}</span>
|
|
||||||
<span class="pr-number">#${pr.pr_number}</span>
|
|
||||||
</div>
|
|
||||||
<h3 class="pr-title">
|
|
||||||
<a href="${pr.pr_url}" target="_blank">${pr.pr_title}</a>
|
|
||||||
</h3>
|
|
||||||
<div class="pr-meta">
|
|
||||||
<span class="pr-author">by ${pr.author}</span>
|
|
||||||
<span class="pr-date">${new Date(pr.created_at).toLocaleDateString()}</span>
|
|
||||||
</div>
|
|
||||||
<div class="pr-labels">
|
|
||||||
${pr.labels.map(label =>
|
|
||||||
`<span class="label" style="background-color: #${label.color}">${label.name}</span>`
|
|
||||||
).join('')}
|
|
||||||
</div>
|
|
||||||
${pr.draft ? '<span class="badge draft-badge">Draft</span>' : ''}
|
|
||||||
${!pr.mergeable ? '<span class="badge conflict-badge">Merge Conflict</span>' : ''}
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading Gitea reviews:', error);
|
|
||||||
document.getElementById('reviews-list').innerHTML =
|
|
||||||
`<p class="error">Failed to load reviews: ${error.message}</p>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadGiteaActivity() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/gitea/activity');
|
|
||||||
const activities = await response.json();
|
|
||||||
giteaData.activity = activities;
|
|
||||||
|
|
||||||
const activityFeed = document.getElementById('activity-feed');
|
|
||||||
if (activities.length === 0) {
|
|
||||||
activityFeed.innerHTML = '<p class="empty">No recent activity</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
activityFeed.innerHTML = activities.map(act => `
|
|
||||||
<div class="activity-item">
|
|
||||||
<div class="activity-icon">${getActivityIcon(act.op_type)}</div>
|
|
||||||
<div class="activity-content">
|
|
||||||
<div class="activity-header">
|
|
||||||
<a href="${act.repo_url}" class="activity-repo">${act.repo}</a>
|
|
||||||
<span class="activity-time">${timeAgo(act.created_at)}</span>
|
|
||||||
</div>
|
|
||||||
<div class="activity-desc">
|
|
||||||
${act.content || act.op_type}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading Gitea activity:', error);
|
|
||||||
document.getElementById('activity-feed').innerHTML =
|
|
||||||
`<p class="error">Failed to load activity: ${error.message}</p>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActivityIcon(type) {
|
|
||||||
const icons = {
|
|
||||||
'create': '✨',
|
|
||||||
'push': '📤',
|
|
||||||
'merge': '🔀',
|
|
||||||
'close': '✅',
|
|
||||||
'reopen': '🔄',
|
|
||||||
'comment': '💬',
|
|
||||||
'label': '🏷️',
|
|
||||||
'milestone': '🎯',
|
|
||||||
'assign': '👤',
|
|
||||||
'review': '👀',
|
|
||||||
'release': '🚀'
|
|
||||||
};
|
|
||||||
return icons[type] || '📝';
|
|
||||||
}
|
|
||||||
|
|
||||||
function timeAgo(dateString) {
|
|
||||||
const now = new Date();
|
|
||||||
const date = new Date(dateString);
|
|
||||||
const seconds = Math.floor((now - date) / 1000);
|
|
||||||
|
|
||||||
if (seconds < 60) return 'just now';
|
|
||||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
||||||
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
|
|
||||||
if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`;
|
|
||||||
return date.toLocaleDateString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-load Gitea data on page load
|
|
||||||
if (document.querySelector('.gitea-dashboard')) {
|
|
||||||
loadGiteaSwarm();
|
|
||||||
|
|
||||||
// Refresh every 30 seconds
|
|
||||||
setInterval(() => {
|
|
||||||
const activeTab = document.querySelector('.gitea-tabs .tab-btn.active');
|
|
||||||
if (activeTab) {
|
|
||||||
const tabName = activeTab.dataset.tab;
|
|
||||||
if (tabName === 'swarm') loadGiteaSwarm();
|
|
||||||
else if (tabName === 'reviews') loadGiteaReviews();
|
|
||||||
else if (tabName === 'activity') loadGiteaActivity();
|
|
||||||
}
|
|
||||||
}, 30000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ GITEA PAGE ============
|
|
||||||
let giteaData = { repos: [], prs: [], activity: [] };
|
|
||||||
|
|
||||||
function initGiteaPage() {
|
|
||||||
loadGiteaData();
|
|
||||||
|
|
||||||
document.getElementById('refresh-gitea')?.addEventListener('click', loadGiteaData);
|
|
||||||
|
|
||||||
// Tab switching
|
|
||||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', (e) => {
|
|
||||||
const tab = e.target.dataset.tab;
|
|
||||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
||||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
||||||
e.target.classList.add('active');
|
|
||||||
document.getElementById(`tab-${tab}`)?.classList.add('active');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadGiteaData() {
|
|
||||||
try {
|
|
||||||
const [swarmRes, reviewsRes, activityRes] = await Promise.all([
|
|
||||||
fetch('/api/gitea/swarm'),
|
|
||||||
fetch('/api/gitea/reviews'),
|
|
||||||
fetch('/api/gitea/activity')
|
|
||||||
]);
|
|
||||||
|
|
||||||
giteaData.repos = await swarmRes.json();
|
|
||||||
giteaData.prs = await reviewsRes.json();
|
|
||||||
giteaData.activity = await activityRes.json();
|
|
||||||
|
|
||||||
renderGiteaStats();
|
|
||||||
renderGiteaRepos();
|
|
||||||
renderGiteaPRs();
|
|
||||||
renderGiteaActivity();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load Gitea data:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderGiteaStats() {
|
|
||||||
document.getElementById('stat-repos').textContent = giteaData.repos.length;
|
|
||||||
|
|
||||||
const totalPRs = giteaData.repos.reduce((sum, r) => sum + (r.open_prs || 0), 0);
|
|
||||||
document.getElementById('stat-prs').textContent = totalPRs;
|
|
||||||
|
|
||||||
const pendingReviews = giteaData.prs.filter(p => p.review_required).length;
|
|
||||||
document.getElementById('stat-reviews').textContent = pendingReviews;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderGiteaRepos() {
|
|
||||||
const grid = document.getElementById('repos-grid');
|
|
||||||
if (!grid) return;
|
|
||||||
|
|
||||||
if (!giteaData.repos.length) {
|
|
||||||
grid.innerHTML = '<p class="no-data">No repositories found</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
grid.innerHTML = giteaData.repos.map(repo => `
|
|
||||||
<div class="repo-card">
|
|
||||||
<h4>${escapeHtml(repo.name)}</h4>
|
|
||||||
<p class="repo-fullname">${escapeHtml(repo.full_name || '')}</p>
|
|
||||||
<div class="repo-meta">
|
|
||||||
<span>⭐ ${repo.stars || 0}</span>
|
|
||||||
<span>🔀 ${repo.open_prs || 0}</span>
|
|
||||||
<span>🐛 ${repo.open_issues || 0}</span>
|
|
||||||
</div>
|
|
||||||
<div class="repo-updated">Updated: ${repo.updated_at ? new Date(repo.updated_at).toLocaleDateString() : 'N/A'}</div>
|
|
||||||
<a href="${repo.html_url}" target="_blank" class="btn-secondary">View →</a>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderGiteaPRs() {
|
|
||||||
const list = document.getElementById('prs-list');
|
|
||||||
if (!list) return;
|
|
||||||
|
|
||||||
// Collect all PRs from all repos
|
|
||||||
const allPRs = [];
|
|
||||||
giteaData.repos.forEach(repo => {
|
|
||||||
if (repo.prs) {
|
|
||||||
repo.prs.forEach(pr => {
|
|
||||||
allPRs.push({ ...pr, repo: repo.name });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!allPRs.length) {
|
|
||||||
list.innerHTML = '<p class="no-data">No open pull requests</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
list.innerHTML = allPRs.map(pr => `
|
|
||||||
<div class="pr-item">
|
|
||||||
<div class="pr-header">
|
|
||||||
<span class="pr-number">#${pr.number}</span>
|
|
||||||
<span class="pr-title">${escapeHtml(pr.title)}</span>
|
|
||||||
<span class="pr-status ${pr.state}">${pr.state}</span>
|
|
||||||
</div>
|
|
||||||
<div class="pr-meta">
|
|
||||||
<span>📦 ${pr.repo}</span>
|
|
||||||
<span>👤 ${pr.user?.login || 'Unknown'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderGiteaActivity() {
|
|
||||||
const list = document.getElementById('activity-list');
|
|
||||||
if (!list) return;
|
|
||||||
|
|
||||||
if (!giteaData.activity || !giteaData.activity.length) {
|
|
||||||
list.innerHTML = '<p class="no-data">No recent activity</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
list.innerHTML = giteaData.activity.slice(0, 20).map(activity => `
|
|
||||||
<div class="activity-item">
|
|
||||||
<span class="activity-type">${activity.type || '📝'}</span>
|
|
||||||
<span class="activity-desc">${escapeHtml(activity.message || activity.repo?.name || 'Activity')}</span>
|
|
||||||
<span class="activity-time">${activity.created_at ? new Date(activity.created_at).toLocaleString() : ''}</span>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ DRAG AND DROP ============
|
|
||||||
let draggedTask = null;
|
|
||||||
|
|
||||||
function initDragAndDrop() {
|
|
||||||
const board = document.getElementById('board');
|
|
||||||
if (!board) return;
|
|
||||||
|
|
||||||
// Add drag listeners to columns
|
|
||||||
document.querySelectorAll('.column').forEach(column => {
|
|
||||||
column.addEventListener('dragover', handleDragOver);
|
|
||||||
column.addEventListener('drop', handleDrop);
|
|
||||||
column.addEventListener('dragleave', handleDragLeave);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDragOver(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.currentTarget.classList.add('drag-over');
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDragLeave(e) {
|
|
||||||
e.currentTarget.classList.remove('drag-over');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDrop(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const column = e.currentTarget;
|
|
||||||
column.classList.remove('drag-over');
|
|
||||||
|
|
||||||
if (!draggedTask) return;
|
|
||||||
|
|
||||||
const newStatus = column.dataset.status;
|
|
||||||
if (newStatus === draggedTask.status) return;
|
|
||||||
|
|
||||||
// Update task status via API
|
|
||||||
try {
|
|
||||||
await fetch(`/api/tasks/${draggedTask.id}`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ status: newStatus })
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reload tasks
|
|
||||||
await initDragAndDrop();
|
|
||||||
loadTasks();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to update task:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
draggedTask = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeTaskCardDraggable(cardEl, task) {
|
|
||||||
cardEl.draggable = true;
|
|
||||||
|
|
||||||
cardEl.addEventListener('dragstart', (e) => {
|
|
||||||
draggedTask = task;
|
|
||||||
cardEl.classList.add('dragging');
|
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
|
||||||
});
|
|
||||||
|
|
||||||
cardEl.addEventListener('dragend', () => {
|
|
||||||
cardEl.classList.remove('dragging');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ WEBSOCKET REAL-TIME UPDATES ============
|
|
||||||
let ws = null;
|
|
||||||
let reconnectAttempts = 0;
|
|
||||||
const MAX_RECONNECT = 5;
|
|
||||||
|
|
||||||
function initWebSocket() {
|
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}`;
|
|
||||||
|
|
||||||
ws = new WebSocket(wsUrl);
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
|
||||||
console.log('WebSocket connected');
|
|
||||||
reconnectAttempts = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
|
||||||
try {
|
|
||||||
const { type, payload } = JSON.parse(event.data);
|
|
||||||
handleWebSocketMessage(type, payload);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('WebSocket message error:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
console.log('WebSocket disconnected');
|
|
||||||
if (reconnectAttempts < MAX_RECONNECT) {
|
|
||||||
reconnectAttempts++;
|
|
||||||
setTimeout(initWebSocket, 2000 * reconnectAttempts);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (err) => {
|
|
||||||
console.error('WebSocket error:', err);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleWebSocketMessage(type, payload) {
|
|
||||||
switch (type) {
|
|
||||||
case 'task_created':
|
|
||||||
if (typeof loadTasks === 'function' && CURRENT_PAGE === 'tasks') {
|
|
||||||
loadTasks();
|
|
||||||
showNotification(`New task: ${payload.title}`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'task_updated':
|
|
||||||
if (typeof loadTasks === 'function' && CURRENT_PAGE === 'tasks') {
|
|
||||||
loadTasks();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'task_deleted':
|
|
||||||
if (typeof loadTasks === 'function' && CURRENT_PAGE === 'tasks') {
|
|
||||||
loadTasks();
|
|
||||||
showNotification('Task deleted');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'usage_updated':
|
|
||||||
if (typeof loadUsage === 'function' && CURRENT_PAGE === 'usage') {
|
|
||||||
loadUsage();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.log('Unknown WebSocket message:', type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showNotification(message) {
|
|
||||||
// Create a simple toast notification
|
|
||||||
const toast = document.createElement('div');
|
|
||||||
toast.className = 'toast-notification';
|
|
||||||
toast.textContent = message;
|
|
||||||
document.body.appendChild(toast);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.classList.add('fade-out');
|
|
||||||
setTimeout(() => toast.remove(), 300);
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize WebSocket on page load
|
|
||||||
if (typeof CURRENT_PAGE !== 'undefined') {
|
|
||||||
initWebSocket();
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user