fix: add date filtering to usage/real endpoint and export

This commit is contained in:
2026-03-04 15:36:44 -08:00
parent 2a0788ee04
commit 65b1b42a79
12 changed files with 3833 additions and 1149 deletions

View File

@@ -599,7 +599,14 @@ function initAgentsPage() {
async function loadUsage() {
// Try real usage first, fallback to tracked usage
try {
const realRes = await fetch("/api/usage/real");
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 = {
@@ -829,7 +836,7 @@ function initUsagePage() {
exportJsonBtn.addEventListener('click', () => {
const fromValue = fromInput.value;
const toValue = toInput.value;
let url = '/api/usage/export?format=json';
let url = '/api/usage/export/real?format=json';
if (fromValue) url += `&from=${fromValue}`;
if (toValue) url += `&to=${toValue}`;
window.open(url, '_blank');
@@ -838,7 +845,7 @@ function initUsagePage() {
exportCsvBtn.addEventListener('click', () => {
const fromValue = fromInput.value;
const toValue = toInput.value;
let url = '/api/usage/export?format=csv';
let url = '/api/usage/export/real?format=csv';
if (fromValue) url += `&from=${fromValue}`;
if (toValue) url += `&to=${toValue}`;
window.open(url, '_blank');
@@ -881,3 +888,208 @@ document.addEventListener("DOMContentLoaded", () => {
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 = '<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);
}

View File

@@ -4,11 +4,12 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenClaw Agent Fleet Dashboard</title>
<!-- Distinctive Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Outfit:wght@300;400;500;600;700;800;900&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
<!-- Marked.js for markdown rendering -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Chart.js for usage charts -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<div class="container">
@@ -19,12 +20,11 @@
<a href="#wiki" class="nav-link" data-page="wiki">Wiki</a>
<a href="#agents" class="nav-link" data-page="agents">Agents</a>
<a href="#usage" class="nav-link" data-page="usage">Usage</a>
<button id="theme-toggle" class="btn-secondary" type="button" aria-label="Toggle theme">Dark Mode</button>
<a href="#gitea" class="nav-link" data-page="gitea">Gitea</a>
</nav>
</header>
<main>
<!-- TASKS PAGE -->
<section id="page-tasks" class="page active">
<div class="composer">
<h2>Create Task</h2>
@@ -84,137 +84,71 @@
</div>
</section>
<!-- WIKI PAGE -->
<section id="page-wiki" class="page">
<div class="wiki-container">
<div class="wiki-sidebar">
<div class="wiki-actions">
<button id="wiki-new-btn" class="btn-primary">+ New Page</button>
</div>
<div class="wiki-search">
<input type="text" id="wiki-search" placeholder="Search wiki...">
</div>
<div id="wiki-list" class="wiki-list"></div>
</div>
<div class="wiki-main">
<div class="wiki-toolbar">
<div class="wiki-page-title" id="wiki-page-title">Select a page</div>
<div class="wiki-page-actions" id="wiki-page-actions" style="display: none;">
<button id="wiki-edit-btn" class="btn-secondary">Edit</button>
<button id="wiki-delete-btn" class="btn-danger">Delete</button>
</div>
</div>
<div id="wiki-content" class="wiki-content">
<div class="wiki-placeholder">
<p>📚 Select a wiki page from the sidebar or create a new one.</p>
</div>
</div>
<div id="wiki-editor" class="wiki-editor" style="display: none;">
<input type="text" id="wiki-edit-title" placeholder="Page title">
<textarea id="wiki-edit-content" placeholder="Markdown content..."></textarea>
<div class="editor-actions">
<button id="wiki-save-btn" class="btn-primary">Save</button>
<button id="wiki-cancel-btn" class="btn-secondary">Cancel</button>
</div>
</div>
</div>
</div>
<div id="wiki-list"></div>
<div id="wiki-content"></div>
</section>
<!-- AGENTS PAGE -->
<section id="page-agents" class="page">
<div class="agents-header">
<h2>Agent Fleet</h2>
<div class="agents-controls">
<input type="text" id="agent-search" placeholder="Search agents...">
<select id="agent-status-filter">
<option value="">All Statuses</option>
<option value="active">Active</option>
<option value="busy">Busy</option>
<option value="idle">Idle</option>
</select>
</div>
</div>
<div id="agents-grid" class="agents-grid"></div>
<!-- Agent Details Modal -->
<div id="agent-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-agent-name">Agent Details</h3>
<button class="modal-close" id="modal-close">&times;</button>
</div>
<div class="modal-body" id="modal-agent-body"></div>
</div>
</div>
<!-- Assign Task Modal -->
<div id="assign-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Assign Task to <span id="assign-agent-name"></span></h3>
<button class="modal-close" id="assign-modal-close">&times;</button>
</div>
<div class="modal-body">
<select id="assign-task-select">
<option value="">Select a task...</option>
</select>
<button id="confirm-assign-btn" class="btn-primary">Assign Task</button>
</div>
</div>
</div>
<div id="agents-grid"></div>
</section>
<!-- USAGE PAGE -->
<section id="page-usage" class="page">
<div class="usage-header">
<h2>API Usage & Statistics</h2>
<div class="usage-controls">
<div class="date-range">
<label>From:</label>
<input type="date" id="usage-from">
<label>To:</label>
<input type="date" id="usage-to">
<button id="usage-apply-filter" class="btn-secondary">Apply</button>
</div>
<div class="export-actions">
<button id="export-json" class="btn-secondary">Export JSON</button>
<button id="export-csv" class="btn-secondary">Export CSV</button>
</div>
</div>
</div>
<div class="usage-stats">
<div class="stat-card">
<h4>Total Requests</h4>
<div class="stat-value" id="stat-requests">0</div>
</div>
<div class="stat-card">
<h4>Total Tokens</h4>
<div class="stat-value" id="stat-tokens">0</div>
</div>
<div class="stat-card">
<h4>Estimated Cost</h4>
<div class="stat-value" id="stat-cost">$0.00</div>
</div>
</div>
<div class="usage-charts">
<div class="chart-container">
<h4>Usage by Provider</h4>
<canvas id="chart-provider"></canvas>
</div>
<div class="chart-container">
<h4>Usage by Agent</h4>
<canvas id="chart-agent"></canvas>
</div>
</div>
<div id="usage-data" class="usage-details">
<h3>Provider Details</h3>
<div class="usage-grid" id="provider-grid"></div>
</div>
<div id="usage-data"></div>
</section>
<section id="page-gitea" class="page">
<h2>🔧 Gitea Integration</h2>
<div class="gitea-dashboard">
<div class="gitea-tabs">
<button class="tab-btn active" data-tab="swarm">Swarm Overview</button>
<button class="tab-btn" data-tab="reviews">Pending Reviews</button>
<button class="tab-btn" data-tab="activity">Recent Activity</button>
</div>
<div class="gitea-content">
<!-- Swarm Overview Tab -->
<div id="swarm-tab" class="tab-content active">
<div class="swarm-stats">
<div class="stat-card">
<h3>Total Repos</h3>
<div class="stat-value" id="total-repos">-</div>
</div>
<div class="stat-card">
<h3>Open PRs</h3>
<div class="stat-value" id="total-prs">-</div>
</div>
<div class="stat-card">
<h3>Open Issues</h3>
<div class="stat-value" id="total-issues">-</div>
</div>
<div class="stat-card">
<h3>Total Branches</h3>
<div class="stat-value" id="total-branches">-</div>
</div>
</div>
<div class="repo-list" id="repo-list">
<p class="loading">Loading repositories...</p>
</div>
</div>
<!-- Pending Reviews Tab -->
<div id="reviews-tab" class="tab-content">
<div class="reviews-list" id="reviews-list">
<p class="loading">Loading pending reviews...</p>
</div>
</div>
<!-- Recent Activity Tab -->
<div id="activity-tab" class="tab-content">
<div class="activity-feed" id="activity-feed">
<p class="loading">Loading recent activity...</p>
</div>
</div>
</div>
</div>
</section>
</main>
</div>
<script src="app.js"></script>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff