// ============ THEME ============
const THEME_STORAGE_KEY = 'agentdash-theme';
const themeToggleBtn = document.getElementById('theme-toggle');
const systemThemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
function getSystemTheme() {
return systemThemeMedia.matches ? 'dark' : 'light';
}
function getSavedTheme() {
try {
const saved = localStorage.getItem(THEME_STORAGE_KEY);
return saved === 'light' || saved === 'dark' ? saved : null;
} catch {
return null;
}
}
function setSavedTheme(theme) {
try {
localStorage.setItem(THEME_STORAGE_KEY, theme);
} catch {
// Ignore localStorage errors (privacy mode, quota, etc.).
}
}
function updateThemeToggleLabel() {
if (!themeToggleBtn) return;
const isDarkTheme = document.documentElement.getAttribute('data-theme') === 'dark';
const nextTheme = isDarkTheme ? 'light' : 'dark';
themeToggleBtn.textContent = isDarkTheme ? 'Light Mode' : 'Dark Mode';
themeToggleBtn.setAttribute('aria-label', `Switch to ${nextTheme} mode`);
}
function applyTheme(theme, { persist = false } = {}) {
document.documentElement.setAttribute('data-theme', theme);
if (persist) setSavedTheme(theme);
updateThemeToggleLabel();
if (usageStats) renderUsageCharts();
}
function initTheme() {
const savedTheme = getSavedTheme();
applyTheme(savedTheme || getSystemTheme());
if (themeToggleBtn) {
themeToggleBtn.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme') || getSystemTheme();
const nextTheme = currentTheme === 'dark' ? 'light' : 'dark';
applyTheme(nextTheme, { persist: true });
});
}
systemThemeMedia.addEventListener('change', (event) => {
if (!getSavedTheme()) {
applyTheme(event.matches ? 'dark' : 'light');
}
});
}
// ============ STATE ============
const COLUMNS = {
Backlog: { title: '📋 Backlog', tasks: [] },
Todo: { title: '📝 Todo', tasks: [] },
'In Progress': { title: '🔄 In Progress', tasks: [] },
Review: { title: '👀 Review', tasks: [] },
Done: { title: '✅ Done', tasks: [] },
};
let wikiPages = [];
let currentWikiPage = null;
let allAgents = [];
let usageStats = null;
let providerChart = null;
let agentChart = null;
// ============ TASK DASHBOARD ============
async function loadTasks() {
const res = await fetch('/api/tasks');
const tasks = await res.json();
Object.keys(COLUMNS).forEach((status) => {
COLUMNS[status].tasks = [];
});
tasks.forEach((task) => {
if (COLUMNS[task.status]) {
COLUMNS[task.status].tasks.push(task);
}
});
renderBoard();
}
function renderBoard() {
const board = document.getElementById('board');
if (!board) return;
board.innerHTML = '';
Object.entries(COLUMNS).forEach(([status, column]) => {
const columnEl = document.createElement('div');
columnEl.className = 'column';
columnEl.innerHTML = `
${column.title}
${column.tasks.length}
`;
const cardsEl = columnEl.querySelector('.cards');
column.tasks.forEach((task) => {
const cardEl = document.createElement('div');
cardEl.className = 'card';
cardEl.innerHTML = `
🧠 ${agent.model || 'unknown'}
⏰ ${agent.lastActivity ? new Date(agent.lastActivity).toLocaleString() : 'Never'}
📋 ${agent.workload} active task${agent.workload !== 1 ? 's' : ''}
📋 Current Task
${agent.currentTask || 'No active task'}
🛠️ Tools
${agent.tools.length ? agent.tools.slice(0, 5).map((tool) => \`\${escapeHtml(tool)}\`).join('') : 'No tools'}
${agent.tools.length > 5 ? \`+\${agent.tools.length - 5} more\` : ''}
📄 Recent Files
${agent.files.length ? agent.files.slice(0, 5).map((file) => \`\${escapeHtml(file)}\`).join('') : 'No files'}
`${escapeHtml(t)}`).join(' ')}
`;
const checkbox = cardEl.querySelector('.card-check');
checkbox.addEventListener('change', async () => {
await fetch(`/api/tasks/${task.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'Done' }),
});
loadTasks();
});
cardsEl.appendChild(cardEl);
});
board.appendChild(columnEl);
});
}
async function populateAgentDropdown() {
try {
const res = await fetch('/api/agents');
const agents = await res.json();
const select = document.getElementById('assignee');
if (!select) return;
const firstOption = select.options[0];
select.innerHTML = '';
select.appendChild(firstOption);
agents.forEach((agent) => {
const option = document.createElement('option');
option.value = agent.name;
option.textContent = agent.name;
select.appendChild(option);
});
} catch (err) {
console.error('Failed to load agents for dropdown:', err);
}
}
function initTasksPage() {
const taskForm = document.getElementById('task-form');
if (!taskForm) return;
taskForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const tagsValue = formData.get('tags');
const task = {
title: formData.get('title'),
description: formData.get('description'),
assignee: formData.get('assignee'),
priority: formData.get('priority'),
status: formData.get('status') || 'Backlog',
tags: (tagsValue || '').split(',').map((t) => t.trim()).filter((t) => t),
};
await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(task),
});
e.target.reset();
loadTasks();
});
populateAgentDropdown();
loadTasks();
}
// ============ WIKI ============
async function loadWiki() {
try {
const res = await fetch('/api/wiki');
wikiPages = await res.json();
renderWikiList();
} catch (err) {
console.error('Failed to load wiki:', err);
}
}
function renderWikiList(filter = '') {
const wikiList = document.getElementById('wiki-list');
if (!wikiList) return;
wikiList.innerHTML = '';
const filtered = filter
? wikiPages.filter((p) => p.title.toLowerCase().includes(filter.toLowerCase()) || p.filename.toLowerCase().includes(filter.toLowerCase()))
: wikiPages;
filtered.forEach((page) => {
const itemEl = document.createElement('div');
itemEl.className = `wiki-item${currentWikiPage && currentWikiPage.filename === page.filename ? ' active' : ''}`;
itemEl.innerHTML = `
${escapeHtml(page.title)}
${new Date(page.modified).toLocaleDateString()}
`;
itemEl.addEventListener('click', () => selectWikiPage(page.filename));
wikiList.appendChild(itemEl);
});
}
async function selectWikiPage(filename) {
try {
const res = await fetch(`/api/wiki/${filename}`);
if (!res.ok) throw new Error('Page not found');
currentWikiPage = await res.json();
const titleEl = document.getElementById('wiki-page-title');
const actionsEl = document.getElementById('wiki-page-actions');
const contentEl = document.getElementById('wiki-content');
const editorEl = document.getElementById('wiki-editor');
const searchEl = document.getElementById('wiki-search');
if (!titleEl || !actionsEl || !contentEl || !editorEl || !searchEl) return;
titleEl.textContent = currentWikiPage.metadata.title || filename;
actionsEl.style.display = 'flex';
contentEl.style.display = 'block';
editorEl.style.display = 'none';
if (typeof marked !== 'undefined') {
contentEl.innerHTML = marked.parse(currentWikiPage.content);
} else {
contentEl.innerHTML = `${escapeHtml(currentWikiPage.content)}`;
}
renderWikiList(searchEl.value);
} catch (err) {
console.error('Failed to load wiki page:', err);
}
}
function initWikiPage() {
const searchInput = document.getElementById('wiki-search');
const newBtn = document.getElementById('wiki-new-btn');
const editBtn = document.getElementById('wiki-edit-btn');
const saveBtn = document.getElementById('wiki-save-btn');
const cancelBtn = document.getElementById('wiki-cancel-btn');
const deleteBtn = document.getElementById('wiki-delete-btn');
if (!searchInput || !newBtn || !editBtn || !saveBtn || !cancelBtn || !deleteBtn) return;
searchInput.addEventListener('input', (e) => {
renderWikiList(e.target.value);
});
newBtn.addEventListener('click', async () => {
const title = prompt('Enter page title:');
if (!title) return;
try {
const res = await fetch('/api/wiki', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
});
if (res.ok) {
const data = await res.json();
await loadWiki();
selectWikiPage(data.filename);
}
} catch (err) {
console.error('Failed to create wiki page:', err);
}
});
editBtn.addEventListener('click', () => {
if (!currentWikiPage) return;
const contentEl = document.getElementById('wiki-content');
const editorEl = document.getElementById('wiki-editor');
const titleEl = document.getElementById('wiki-edit-title');
const editContentEl = document.getElementById('wiki-edit-content');
if (!contentEl || !editorEl || !titleEl || !editContentEl) return;
contentEl.style.display = 'none';
editorEl.style.display = 'block';
titleEl.value = currentWikiPage.metadata.title || '';
editContentEl.value = currentWikiPage.content;
});
saveBtn.addEventListener('click', async () => {
if (!currentWikiPage) return;
const editContentEl = document.getElementById('wiki-edit-content');
if (!editContentEl) return;
try {
const res = await fetch(`/api/wiki/${currentWikiPage.filename}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: editContentEl.value }),
});
if (res.ok) {
await selectWikiPage(currentWikiPage.filename);
}
} catch (err) {
console.error('Failed to save wiki page:', err);
}
});
cancelBtn.addEventListener('click', () => {
const editorEl = document.getElementById('wiki-editor');
const contentEl = document.getElementById('wiki-content');
if (!editorEl || !contentEl) return;
editorEl.style.display = 'none';
contentEl.style.display = 'block';
});
deleteBtn.addEventListener('click', async () => {
if (!currentWikiPage) return;
if (!confirm(`Delete "${currentWikiPage.metadata.title || currentWikiPage.filename}"?`)) return;
try {
const res = await fetch(`/api/wiki/${currentWikiPage.filename}`, {
method: 'DELETE',
});
if (res.ok) {
currentWikiPage = null;
const pageTitle = document.getElementById('wiki-page-title');
const pageActions = document.getElementById('wiki-page-actions');
const wikiContent = document.getElementById('wiki-content');
if (pageTitle) pageTitle.textContent = 'Select a page';
if (pageActions) pageActions.style.display = 'none';
if (wikiContent) {
wikiContent.innerHTML = '📚 Select a wiki page from the sidebar or create a new one.
';
}
await loadWiki();
}
} catch (err) {
console.error('Failed to delete wiki page:', err);
}
});
loadWiki();
}
// ============ AGENTS ============
async function loadAgents() {
try {
const res = await fetch('/api/agents');
allAgents = await res.json();
renderAgents();
} catch (err) {
console.error('Failed to load agents:', err);
}
}
function renderAgents(filter = '', statusFilter = '') {
const grid = document.getElementById('agents-grid');
if (!grid) return;
grid.innerHTML = '';
let filtered = allAgents;
if (filter) {
filtered = filtered.filter((a) =>
a.name.toLowerCase().includes(filter.toLowerCase()) ||
(a.currentTask && a.currentTask.toLowerCase().includes(filter.toLowerCase()))
);
}
if (statusFilter) {
filtered = filtered.filter((a) => a.status === statusFilter);
}
filtered.forEach((agent) => {
const cardEl = document.createElement('div');
cardEl.className = 'agent-card';
const statusClass = `status-${agent.status}`;
cardEl.innerHTML = `
🧠 ${agent.model || 'unknown'}
⏰ ${agent.lastActivity ? new Date(agent.lastActivity).toLocaleString() : 'Never'}
📋 ${agent.workload} active task${agent.workload !== 1 ? 's' : ''}
📋 Current Task
${agent.currentTask || 'No active task'}
🛠️ Tools
${agent.tools.length ? agent.tools.slice(0, 5).map((tool) => \`\${escapeHtml(tool)}\`).join('') : 'No tools'}
${agent.tools.length > 5 ? \`+\${agent.tools.length - 5} more\` : ''}
📄 Recent Files
${agent.files.length ? agent.files.slice(0, 5).map((file) => \`\${escapeHtml(file)}\`).join('') : 'No files'}
`${escapeHtml(tool)}`).join('') : 'No tools'}
${agent.tools.length > 5 ? `+${agent.tools.length - 5} more` : ''}
📄 Recent Files
${agent.files.length ? agent.files.slice(0, 5).map((file) => `${escapeHtml(file)}`).join('') : 'No files'}
`;
cardEl.querySelector('.agent-details-btn').addEventListener('click', () => showAgentDetails(agent));
cardEl.querySelector('.agent-assign-btn').addEventListener('click', () => showAssignModal(agent.name));
grid.appendChild(cardEl);
});
}
function showAgentDetails(agent) {
const modal = document.getElementById('agent-modal');
const body = document.getElementById('modal-agent-body');
const title = document.getElementById('modal-agent-name');
if (!modal || !body || !title) return;
title.textContent = agent.name;
body.innerHTML = `
Workload
${agent.workload} active task${agent.workload !== 1 ? 's' : ''}
Current Task
${agent.currentTask || 'No active task'}
Active Tasks
${agent.activeTasks.length
? agent.activeTasks.map((t) => `- ${escapeHtml(t.title)} ${t.priority}
`).join('')
: '- No active tasks
'}
Recently Completed
${agent.completedTasks.length
? agent.completedTasks.map((t) => `- ${escapeHtml(t.title)}
`).join('')
: '- No completed tasks
'}
Tools
${agent.tools.length ? agent.tools.map((t) => `${escapeHtml(t)}`).join('') : 'No tools'}
Capabilities
${agent.capabilities.length ? agent.capabilities.map((c) => `${escapeHtml(c)}`).join('') : 'No capabilities defined'}
`;
modal.classList.add('active');
}
async function showAssignModal(agentName) {
const modal = document.getElementById('assign-modal');
const agentNameEl = document.getElementById('assign-agent-name');
const select = document.getElementById('assign-task-select');
if (!modal || !agentNameEl || !select) return;
agentNameEl.textContent = agentName;
try {
const res = await fetch('/api/tasks');
const tasks = await res.json();
const unassignedTasks = tasks.filter((t) => t.status !== 'Done' && (!t.assignee || t.assignee === ''));
select.innerHTML = '';
unassignedTasks.forEach((task) => {
const option = document.createElement('option');
option.value = task.id;
option.textContent = `${task.title} (${task.priority})`;
select.appendChild(option);
});
select.dataset.agent = agentName;
modal.classList.add('active');
} catch (err) {
console.error('Failed to load tasks for assignment:', err);
}
}
function initAgentsPage() {
const searchInput = document.getElementById('agent-search');
const statusFilter = document.getElementById('agent-status-filter');
const closeBtn = document.getElementById('modal-close');
const assignCloseBtn = document.getElementById('assign-modal-close');
const confirmAssignBtn = document.getElementById('confirm-assign-btn');
if (!searchInput || !statusFilter || !closeBtn || !assignCloseBtn || !confirmAssignBtn) return;
searchInput.addEventListener('input', (e) => {
renderAgents(e.target.value, statusFilter.value);
});
statusFilter.addEventListener('change', (e) => {
renderAgents(searchInput.value, e.target.value);
});
closeBtn.addEventListener('click', () => {
const modal = document.getElementById('agent-modal');
if (modal) modal.classList.remove('active');
});
assignCloseBtn.addEventListener('click', () => {
const modal = document.getElementById('assign-modal');
if (modal) modal.classList.remove('active');
});
confirmAssignBtn.addEventListener('click', async () => {
const select = document.getElementById('assign-task-select');
if (!select) return;
const taskId = select.value;
const agentName = select.dataset.agent;
if (!taskId) {
alert('Please select a task');
return;
}
try {
const res = await fetch(`/api/agents/${agentName}/assign`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ taskId: Number.parseInt(taskId, 10) }),
});
if (res.ok) {
const modal = document.getElementById('assign-modal');
if (modal) modal.classList.remove('active');
await loadAgents();
}
} catch (err) {
console.error('Failed to assign task:', err);
}
});
loadAgents();
}
// ============ USAGE ============
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 to = document.getElementById('usage-to')?.value;
let statsUrl = '/api/usage/stats';
const params = [];
if (from) params.push(`from=${from}`);
if (to) params.push(`to=${to}`);
if (params.length) statsUrl += `?${params.join('&')}`;
try {
const statsRes = await fetch(statsUrl);
usageStats = await statsRes.json();
const usageRes = await fetch('/api/usage');
const usageData = await usageRes.json();
renderUsageStats();
renderUsageCharts();
renderProviderDetails(usageData);
} catch (err) {
console.error('Failed to load usage:', err);
}
}
function renderRealUsageCharts(data) {
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]) => `
${model}
Requests: ${stats.requests || 0}
Tokens: ${(stats.input || 0) + (stats.output || 0)}
`).join('');
}
}
const requestsEl = document.getElementById('stat-requests');
const tokensEl = document.getElementById('stat-tokens');
const costEl = document.getElementById('stat-cost');
if (!requestsEl || !tokensEl || !costEl || !usageStats) return;
requestsEl.textContent = usageStats.totalRequests.toLocaleString();
tokensEl.textContent = usageStats.totalTokens.toLocaleString();
costEl.textContent = `$${usageStats.totalCost.toFixed(2)}`;
}
function renderUsageCharts() {
if (!usageStats || typeof Chart === 'undefined') return;
const providerCanvas = document.getElementById('chart-provider');
const agentCanvas = document.getElementById('chart-agent');
if (!providerCanvas || !agentCanvas) return;
const providerCtx = providerCanvas.getContext('2d');
const agentCtx = agentCanvas.getContext('2d');
if (!providerCtx || !agentCtx) return;
if (providerChart) providerChart.destroy();
const providerLabels = Object.keys(usageStats.byProvider);
const providerData = providerLabels.map((p) => usageStats.byProvider[p].requests);
providerChart = new Chart(providerCtx, {
type: 'doughnut',
data: {
labels: providerLabels,
datasets: [{
data: providerData,
backgroundColor: ['#3498db', '#2ecc71', '#e74c3c', '#f39c12', '#9c27b0', '#00bcd4'],
}],
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
labels: { color: '#e0e0e0' },
},
},
},
});
if (agentChart) agentChart.destroy();
const agentLabels = Object.keys(usageStats.byAgent);
const agentData = agentLabels.map((a) => usageStats.byAgent[a].requests);
agentChart = new Chart(agentCtx, {
type: 'bar',
data: {
labels: agentLabels,
datasets: [{
label: 'Requests',
data: agentData,
backgroundColor: '#3498db',
}],
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
y: {
beginAtZero: true,
ticks: { color: '#e0e0e0' },
grid: { color: '#444' },
},
x: {
ticks: { color: '#e0e0e0' },
grid: { color: '#444' },
},
},
},
});
}
function renderProviderDetails(usageData) {
const grid = document.getElementById('provider-grid');
if (!grid || !usageData || !usageStats) return;
grid.innerHTML = '';
usageData.providers.forEach((provider) => {
const providerEl = document.createElement('div');
providerEl.className = 'provider-card';
const providerUsage = usageStats.byProvider[provider.name] || { requests: 0, tokens: 0, cost: 0 };
providerEl.innerHTML = `
${escapeHtml(provider.name)}
Requests
${providerUsage.requests.toLocaleString()}
Tokens
${providerUsage.tokens.toLocaleString()}
Cost
$${providerUsage.cost.toFixed(2)}
Models
${provider.models.map((model) => `
${escapeHtml(model.name)}
${escapeHtml(model.type)}
`).join('')}
`;
grid.appendChild(providerEl);
});
}
function initUsagePage() {
const fromInput = document.getElementById('usage-from');
const toInput = document.getElementById('usage-to');
const applyBtn = document.getElementById('usage-apply-filter');
const exportJsonBtn = document.getElementById('export-json');
const exportCsvBtn = document.getElementById('export-csv');
if (!fromInput || !toInput || !applyBtn || !exportJsonBtn || !exportCsvBtn) return;
const to = new Date();
const from = new Date();
from.setDate(from.getDate() - 30);
fromInput.value = from.toISOString().split('T')[0];
toInput.value = to.toISOString().split('T')[0];
applyBtn.addEventListener('click', loadUsage);
exportJsonBtn.addEventListener('click', () => {
const fromValue = fromInput.value;
const toValue = toInput.value;
let url = '/api/usage/export/real?format=json';
if (fromValue) url += `&from=${fromValue}`;
if (toValue) url += `&to=${toValue}`;
window.open(url, '_blank');
});
exportCsvBtn.addEventListener('click', () => {
const fromValue = fromInput.value;
const toValue = toInput.value;
let url = '/api/usage/export/real?format=csv';
if (fromValue) url += `&from=${fromValue}`;
if (toValue) url += `&to=${toValue}`;
window.open(url, '_blank');
});
loadUsage();
}
// ============ HELPERS ============
function escapeHtml(text) {
if (typeof text !== 'string') return '';
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return text.replace(/[&<>"']/g, (m) => map[m]);
}
function setupModalBackdropClose() {
document.querySelectorAll('.modal').forEach((modal) => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('active');
}
});
});
}
// ============ INITIALIZATION ============
document.addEventListener("DOMContentLoaded", () => {
const CURRENT_PAGE = document.body?.dataset?.page || "tasks";
initTheme();
setupModalBackdropClose();
if (CURRENT_PAGE === 'tasks') initTasksPage();
if (CURRENT_PAGE === 'wiki') initWikiPage();
if (CURRENT_PAGE === 'agents') initAgentsPage();
if (CURRENT_PAGE === 'usage') initUsagePage();
});
// ============ 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 = 'No repositories found
';
return;
}
repoList.innerHTML = repos.map(repo => `
🔀 ${repo.open_prs} PRs
🐛 ${repo.open_issues} Issues
🌿 ${repo.branches} Branches
`).join('');
} catch (error) {
console.error('Error loading Gitea swarm:', error);
document.getElementById('repo-list').innerHTML =
`Failed to load repositories: ${error.message}
`;
}
}
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 = 'No pending reviews
';
return;
}
reviewsList.innerHTML = reviews.map(pr => `
by ${pr.author}
${new Date(pr.created_at).toLocaleDateString()}
${pr.labels.map(label =>
`${label.name}`
).join('')}
${pr.draft ? '
Draft' : ''}
${!pr.mergeable ? '
Merge Conflict' : ''}
`).join('');
} catch (error) {
console.error('Error loading Gitea reviews:', error);
document.getElementById('reviews-list').innerHTML =
`Failed to load reviews: ${error.message}
`;
}
}
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 = 'No recent activity
';
return;
}
activityFeed.innerHTML = activities.map(act => `
${getActivityIcon(act.op_type)}
${act.content || act.op_type}
`).join('');
} catch (error) {
console.error('Error loading Gitea activity:', error);
document.getElementById('activity-feed').innerHTML =
`Failed to load activity: ${error.message}
`;
}
}
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);
}