fix: add date filtering to usage/real endpoint and export
This commit is contained in:
110
gitea-append.js
Normal file
110
gitea-append.js
Normal file
@@ -0,0 +1,110 @@
|
||||
|
||||
// ============ GITEA INTEGRATION ============
|
||||
const GiteaIntegration = require('./gitea-integration.js');
|
||||
|
||||
const giteaConfig = {
|
||||
baseUrl: process.env.GITEA_URL || 'https://gitea.tophermayor.com',
|
||||
token: process.env.GITEA_TOKEN,
|
||||
owner: 'TopherMayor',
|
||||
cacheTimeout: 30000
|
||||
};
|
||||
|
||||
const gitea = new GiteaIntegration(giteaConfig);
|
||||
|
||||
// Gitea page route
|
||||
app.get('/gitea', (req, res) => {
|
||||
res.send(renderPage('gitea', 'gitea', 'OpenClaw Agent Fleet Dashboard - Gitea'));
|
||||
});
|
||||
|
||||
// Gitea API routes
|
||||
app.get('/api/gitea/swarm', async (req, res) => {
|
||||
try {
|
||||
const summary = await gitea.getSwarmSummary();
|
||||
res.json(summary);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/reviews', async (req, res) => {
|
||||
try {
|
||||
const reviews = await gitea.getPendingReviews();
|
||||
res.json(reviews);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/activity', async (req, res) => {
|
||||
try {
|
||||
const activity = await gitea.getRecentActivity();
|
||||
res.json(activity);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/user', async (req, res) => {
|
||||
try {
|
||||
const user = await gitea.getUser();
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/repos/:repo', async (req, res) => {
|
||||
try {
|
||||
const repo = await gitea.getRepo(req.params.repo);
|
||||
res.json(repo);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/repos/:repo/pulls', async (req, res) => {
|
||||
try {
|
||||
const state = req.query.state || 'open';
|
||||
const prs = await gitea.getPullRequests(req.params.repo, state);
|
||||
res.json(prs);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/repos/:repo/issues', async (req, res) => {
|
||||
try {
|
||||
const state = req.query.state || 'open';
|
||||
const issues = await gitea.getIssues(req.params.repo, state);
|
||||
res.json(issues);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/repos/:repo/commits', async (req, res) => {
|
||||
try {
|
||||
const branch = req.query.branch || 'main';
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
const commits = await gitea.getCommits(req.params.repo, branch, limit);
|
||||
res.json(commits);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/repos/:repo/branches', async (req, res) => {
|
||||
try {
|
||||
const branches = await gitea.getBranches(req.params.repo);
|
||||
res.json(branches);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/gitea/cache/clear', (req, res) => {
|
||||
gitea.clearCache();
|
||||
res.json({ success: true, message: 'Cache cleared' });
|
||||
});
|
||||
|
||||
console.log('✅ Gitea integration loaded');
|
||||
254
gitea-integration.js
Normal file
254
gitea-integration.js
Normal file
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* Gitea Integration for AgentDash
|
||||
* Provides real-time data from Gitea API
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
class GiteaIntegration {
|
||||
constructor(config = {}) {
|
||||
this.baseUrl = config.baseUrl || 'https://gitea.tophermayor.com';
|
||||
this.token = config.token || process.env.GITEA_TOKEN;
|
||||
this.owner = config.owner || 'TopherMayor';
|
||||
this.cache = new Map();
|
||||
this.cacheTimeout = config.cacheTimeout || 30000; // 30 seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Make authenticated request to Gitea API
|
||||
*/
|
||||
async request(endpoint) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(endpoint, this.baseUrl);
|
||||
const client = url.protocol === 'https:' ? https : http;
|
||||
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `token ${this.token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
const req = client.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (e) {
|
||||
reject(new Error(`Failed to parse response: ${e.message}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.setTimeout(10000, () => {
|
||||
req.destroy();
|
||||
reject(new Error('Request timeout'));
|
||||
});
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached data or fetch fresh
|
||||
*/
|
||||
async getCached(key, fetcher) {
|
||||
const cached = this.cache.get(key);
|
||||
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
const data = await fetcher();
|
||||
this.cache.set(key, { data, timestamp: Date.now() });
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all repositories for the owner
|
||||
*/
|
||||
async getRepos() {
|
||||
return this.getCached('repos', () =>
|
||||
this.request(`/api/v1/user/repos?limit=50`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repository details
|
||||
*/
|
||||
async getRepo(repoName) {
|
||||
return this.getCached(`repo:${repoName}`, () =>
|
||||
this.request(`/api/v1/repos/${this.owner}/${repoName}`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get open pull requests for a repository
|
||||
*/
|
||||
async getPullRequests(repoName, state = 'open') {
|
||||
return this.getCached(`prs:${repoName}:${state}`, () =>
|
||||
this.request(`/api/v1/repos/${this.owner}/${repoName}/pulls?state=${state}&limit=20`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get issues for a repository
|
||||
*/
|
||||
async getIssues(repoName, state = 'open') {
|
||||
return this.getCached(`issues:${repoName}:${state}`, () =>
|
||||
this.request(`/api/v1/repos/${this.owner}/${repoName}/issues?state=${state}&limit=20`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent commits for a repository
|
||||
*/
|
||||
async getCommits(repoName, branch = 'main', limit = 10) {
|
||||
return this.getCached(`commits:${repoName}:${branch}`, () =>
|
||||
this.request(`/api/v1/repos/${this.owner}/${repoName}/commits?sha=${branch}&limit=${limit}`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get branches for a repository
|
||||
*/
|
||||
async getBranches(repoName) {
|
||||
return this.getCached(`branches:${repoName}`, () =>
|
||||
this.request(`/api/v1/repos/${this.owner}/${repoName}/branches`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repository activity feed
|
||||
*/
|
||||
async getActivity(repoName) {
|
||||
return this.getCached(`activity:${repoName}`, () =>
|
||||
this.request(`/api/v1/repos/${this.owner}/${repoName}/activities/feeds?limit=20`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user info
|
||||
*/
|
||||
async getUser() {
|
||||
return this.getCached('user', () =>
|
||||
this.request('/api/v1/user')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get organization info (if applicable)
|
||||
*/
|
||||
async getOrgs() {
|
||||
return this.getCached('orgs', () =>
|
||||
this.request('/api/v1/user/orgs')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache
|
||||
*/
|
||||
clearCache() {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get swarm summary - all repos with PRs and issues
|
||||
*/
|
||||
async getSwarmSummary() {
|
||||
const repos = await this.getRepos();
|
||||
const summary = [];
|
||||
|
||||
for (const repo of repos.slice(0, 10)) { // Limit to 10 repos
|
||||
try {
|
||||
const [prs, issues, branches] = await Promise.all([
|
||||
this.getPullRequests(repo.name, 'open').catch(() => []),
|
||||
this.getIssues(repo.name, 'open').catch(() => []),
|
||||
this.getBranches(repo.name).catch(() => [])
|
||||
]);
|
||||
|
||||
summary.push({
|
||||
name: repo.name,
|
||||
full_name: repo.full_name,
|
||||
stars: repo.stars_count || 0,
|
||||
forks: repo.forks_count || 0,
|
||||
open_prs: prs.length,
|
||||
open_issues: issues.length,
|
||||
branches: branches.length,
|
||||
updated_at: repo.updated_at,
|
||||
html_url: repo.html_url
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Error fetching data for ${repo.name}:`, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pending reviews (PRs needing attention)
|
||||
*/
|
||||
async getPendingReviews() {
|
||||
const repos = await this.getRepos();
|
||||
const pending = [];
|
||||
|
||||
for (const repo of repos.slice(0, 10)) {
|
||||
try {
|
||||
const prs = await this.getPullRequests(repo.name, 'open');
|
||||
for (const pr of prs) {
|
||||
pending.push({
|
||||
repo: repo.name,
|
||||
repo_url: repo.html_url,
|
||||
pr_number: pr.number,
|
||||
pr_title: pr.title,
|
||||
pr_url: pr.html_url,
|
||||
author: pr.user?.login,
|
||||
created_at: pr.created_at,
|
||||
mergeable: pr.mergeable,
|
||||
draft: pr.draft,
|
||||
labels: pr.labels || []
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error fetching PRs for ${repo.name}:`, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
return pending;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent activity across all repos
|
||||
*/
|
||||
async getRecentActivity() {
|
||||
const repos = await this.getRepos();
|
||||
const activities = [];
|
||||
|
||||
for (const repo of repos.slice(0, 5)) {
|
||||
try {
|
||||
const activity = await this.getActivity(repo.name);
|
||||
for (const act of (activity || []).slice(0, 5)) {
|
||||
activities.push({
|
||||
repo: repo.name,
|
||||
repo_url: repo.html_url,
|
||||
...act
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip repos without activity access
|
||||
}
|
||||
}
|
||||
|
||||
return activities
|
||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
|
||||
.slice(0, 20);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GiteaIntegration;
|
||||
118
gitea-routes.js
Normal file
118
gitea-routes.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Gitea Routes Module
|
||||
* Adds Gitea integration routes to Express app
|
||||
*/
|
||||
|
||||
const GiteaIntegration = require('./gitea-integration.js');
|
||||
|
||||
function setupGiteaRoutes(app) {
|
||||
// Initialize Gitea client
|
||||
const giteaConfig = {
|
||||
baseUrl: process.env.GITEA_URL || 'https://gitea.tophermayor.com',
|
||||
token: process.env.GITEA_TOKEN,
|
||||
owner: 'TopherMayor',
|
||||
cacheTimeout: 30000
|
||||
};
|
||||
|
||||
const gitea = new GiteaIntegration(giteaConfig);
|
||||
|
||||
// Gitea page route - redirect to main SPA with hash
|
||||
app.get('/gitea', (req, res) => {
|
||||
res.redirect('/#gitea');
|
||||
});
|
||||
|
||||
// Gitea API routes
|
||||
app.get('/api/gitea/swarm', async (req, res) => {
|
||||
try {
|
||||
const summary = await gitea.getSwarmSummary();
|
||||
res.json(summary);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/reviews', async (req, res) => {
|
||||
try {
|
||||
const reviews = await gitea.getPendingReviews();
|
||||
res.json(reviews);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/activity', async (req, res) => {
|
||||
try {
|
||||
const activity = await gitea.getRecentActivity();
|
||||
res.json(activity);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/user', async (req, res) => {
|
||||
try {
|
||||
const user = await gitea.getUser();
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/repos/:repo', async (req, res) => {
|
||||
try {
|
||||
const repo = await gitea.getRepo(req.params.repo);
|
||||
res.json(repo);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/repos/:repo/pulls', async (req, res) => {
|
||||
try {
|
||||
const state = req.query.state || 'open';
|
||||
const prs = await gitea.getPullRequests(req.params.repo, state);
|
||||
res.json(prs);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/repos/:repo/issues', async (req, res) => {
|
||||
try {
|
||||
const state = req.query.state || 'open';
|
||||
const issues = await gitea.getIssues(req.params.repo, state);
|
||||
res.json(issues);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/repos/:repo/commits', async (req, res) => {
|
||||
try {
|
||||
const branch = req.query.branch || 'main';
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
const commits = await gitea.getCommits(req.params.repo, branch, limit);
|
||||
res.json(commits);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/repos/:repo/branches', async (req, res) => {
|
||||
try {
|
||||
const branches = await gitea.getBranches(req.params.repo);
|
||||
res.json(branches);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/gitea/cache/clear', (req, res) => {
|
||||
gitea.clearCache();
|
||||
res.json({ success: true, message: 'Cache cleared' });
|
||||
});
|
||||
|
||||
console.log('✅ Gitea integration loaded');
|
||||
}
|
||||
|
||||
module.exports = { setupGiteaRoutes };
|
||||
218
public/app.js
218
public/app.js
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
1624
public/styles.css
1624
public/styles.css
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
92
server.js
92
server.js
@@ -4,6 +4,7 @@ const fs = require('fs');
|
||||
const http = require('http');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const { WebSocketServer } = require('ws');
|
||||
const { setupGiteaRoutes } = require('./gitea-routes.js');
|
||||
|
||||
const PORT = process.env.PORT || 8395;
|
||||
const DB_PATH = process.env.DB_PATH || path.join(__dirname, 'data', 'tasks.db');
|
||||
@@ -937,6 +938,9 @@ wss.on('connection', (socket) => {
|
||||
socket.send(JSON.stringify({ type: 'connected', payload: { ok: true } }));
|
||||
});
|
||||
|
||||
// Setup Gitea integration
|
||||
setupGiteaRoutes(app, renderPage);
|
||||
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`openclaw-taskboard listening on ${PORT}`);
|
||||
});
|
||||
@@ -946,8 +950,13 @@ const REAL_SESSIONS_DIR = process.env.SESSIONS_DIR || '/app/agents';
|
||||
const SWARM_TASKS_FILE = process.env.SWARM_TASKS_FILE || '/app/swarm/active-tasks.json';
|
||||
|
||||
// GET /api/usage/real - Aggregate usage from session files
|
||||
// GET /api/usage/real - Aggregate usage from session files with date filtering
|
||||
app.get('/api/usage/real', async (req, res) => {
|
||||
try {
|
||||
const { from, to } = req.query;
|
||||
const fromDate = from ? new Date(from) : null;
|
||||
const toDate = to ? new Date(to) : null;
|
||||
|
||||
const usageByAgent = {};
|
||||
const usageByModel = {};
|
||||
let totalInput = 0, totalOutput = 0, totalCost = 0;
|
||||
@@ -975,6 +984,14 @@ app.get('/api/usage/real', async (req, res) => {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const msg = JSON.parse(line);
|
||||
|
||||
// Date filtering
|
||||
if (fromDate || toDate) {
|
||||
const msgDate = new Date(msg.timestamp);
|
||||
if (fromDate && msgDate < fromDate) continue;
|
||||
if (toDate && msgDate > toDate) continue;
|
||||
}
|
||||
|
||||
if (msg.message?.usage) {
|
||||
const u = msg.message.usage;
|
||||
agentInput += u.input || 0;
|
||||
@@ -1014,6 +1031,7 @@ app.get('/api/usage/real', async (req, res) => {
|
||||
total: totalInput + totalOutput,
|
||||
cost: totalCost
|
||||
},
|
||||
filters: { from, to },
|
||||
lastUpdated: new Date().toISOString()
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -1022,7 +1040,79 @@ app.get('/api/usage/real', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/swarm/tasks - Get swarm task registry
|
||||
// GET /api/usage/export/real - Export real usage data
|
||||
app.get('/api/usage/export/real', (req, res) => {
|
||||
const { format = 'json', from, to } = req.query;
|
||||
|
||||
try {
|
||||
const fromDate = from ? new Date(from) : null;
|
||||
const toDate = to ? new Date(to) : null;
|
||||
const usageData = [];
|
||||
|
||||
if (!fs.existsSync(REAL_SESSIONS_DIR)) {
|
||||
return res.status(404).json({ error: 'sessions_dir_not_found' });
|
||||
}
|
||||
|
||||
const agents = fs.readdirSync(REAL_SESSIONS_DIR).filter(d => {
|
||||
return fs.statSync(path.join(REAL_SESSIONS_DIR, d)).isDirectory();
|
||||
});
|
||||
|
||||
for (const agent of agents) {
|
||||
const sessionsDir = path.join(REAL_SESSIONS_DIR, agent, 'sessions');
|
||||
if (!fs.existsSync(sessionsDir)) continue;
|
||||
|
||||
const sessions = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.jsonl'));
|
||||
for (const sessionFile of sessions) {
|
||||
const filePath = path.join(sessionsDir, sessionFile);
|
||||
const lines = fs.readFileSync(filePath, 'utf8').split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const msg = JSON.parse(line);
|
||||
|
||||
if (fromDate || toDate) {
|
||||
const msgDate = new Date(msg.timestamp);
|
||||
if (fromDate && msgDate < fromDate) continue;
|
||||
if (toDate && msgDate > toDate) continue;
|
||||
}
|
||||
|
||||
if (msg.message?.usage) {
|
||||
usageData.push({
|
||||
timestamp: msg.timestamp,
|
||||
agent,
|
||||
model: msg.message.model || 'unknown',
|
||||
provider: msg.message.provider || 'unknown',
|
||||
input: msg.message.usage.input || 0,
|
||||
output: msg.message.usage.output || 0,
|
||||
total: (msg.message.usage.input || 0) + (msg.message.usage.output || 0),
|
||||
cost: msg.message.usage.cost?.total || 0
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (format === 'csv') {
|
||||
const csv = [
|
||||
'timestamp,agent,model,provider,input,output,total,cost',
|
||||
...usageData.map(r => `${r.timestamp},${r.agent},${r.model},${r.provider},${r.input},${r.output},${r.total},${r.cost}`)
|
||||
].join('\n');
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="usage-export.csv"');
|
||||
res.send(csv);
|
||||
} else {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="usage-export.json"');
|
||||
res.json(usageData);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error exporting usage:', err);
|
||||
res.status(500).json({ error: 'failed_to_export_usage' });
|
||||
}
|
||||
});
|
||||
app.get('/api/swarm/tasks', (req, res) => {
|
||||
try {
|
||||
if (fs.existsSync(SWARM_TASKS_FILE)) {
|
||||
|
||||
1165
server.js.broken
Normal file
1165
server.js.broken
Normal file
File diff suppressed because it is too large
Load Diff
54
views/gitea.html
Normal file
54
views/gitea.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<div class="gitea-dashboard">
|
||||
<div class="gitea-header">
|
||||
<h2>🔧 Gitea Integration</h2>
|
||||
<p class="subtitle">Real-time repository and PR tracking</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
@@ -1,31 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{pageTitle}}</title>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
{{markedScript}}
|
||||
{{chartScript}}
|
||||
|
||||
<!-- 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">
|
||||
</head>
|
||||
<body data-page="{{pageName}}">
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>🦞 OpenClaw Agent Fleet Dashboard</h1>
|
||||
<nav>
|
||||
<a href="/tasks" class="nav-link {{tasksActive}}">Tasks</a>
|
||||
<a href="/wiki" class="nav-link {{wikiActive}}">Wiki</a>
|
||||
<a href="/agents" class="nav-link {{agentsActive}}">Agents</a>
|
||||
<a href="/usage" class="nav-link {{usageActive}}">Usage</a>
|
||||
<button id="theme-toggle" class="btn-secondary" type="button" aria-label="Toggle theme">Dark Mode</button>
|
||||
</nav>
|
||||
<body>
|
||||
<!-- Animated background particles -->
|
||||
<div class="bg-particles" aria-hidden="true"></div>
|
||||
|
||||
<!-- Main container -->
|
||||
<div class="app-container">
|
||||
<!-- Header with glassmorphism effect -->
|
||||
<header class="app-header">
|
||||
<div class="header-content">
|
||||
<div class="logo-section">
|
||||
<div class="logo-icon">
|
||||
<span class="logo-emoji">🦞</span>
|
||||
<div class="logo-glow"></div>
|
||||
</div>
|
||||
<div class="logo-text">
|
||||
<h1 class="logo-title">OpenClaw</h1>
|
||||
<p class="logo-subtitle">Fleet Dashboard</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<button id="theme-toggle" class="theme-btn" aria-label="Toggle theme">
|
||||
<span class="theme-icon">☀️</span>
|
||||
<span class="theme-text">Light</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{{content}}
|
||||
|
||||
<!-- Navigation with hover effects -->
|
||||
<nav class="app-nav" role="navigation" aria-label="Main navigation">
|
||||
<div class="nav-container">
|
||||
<a href="/tasks" class="nav-link {{tasksActive}}" data-tab="tasks">
|
||||
<span class="nav-icon">📋</span>
|
||||
<span class="nav-label">Tasks</span>
|
||||
<span class="nav-indicator"></span>
|
||||
</a>
|
||||
<a href="/agents" class="nav-link {{agentsActive}}" data-tab="agents">
|
||||
<span class="nav-icon">🤖</span>
|
||||
<span class="nav-label">Agents</span>
|
||||
<span class="nav-indicator"></span>
|
||||
</a>
|
||||
<a href="/wiki" class="nav-link {{wikiActive}}" data-tab="wiki">
|
||||
<span class="nav-icon">📚</span>
|
||||
<span class="nav-label">Wiki</span>
|
||||
<span class="nav-indicator"></span>
|
||||
</a>
|
||||
<a href="/gitea" class="nav-link {{giteaActive}}" data-tab="gitea">
|
||||
<span class="nav-icon">🔧</span>
|
||||
<span class="nav-label">Gitea</span>
|
||||
<span class="nav-indicator"></span>
|
||||
</a>
|
||||
<a href="/usage" class="nav-link {{usageActive}}" data-tab="usage">
|
||||
<span class="nav-icon">📊</span>
|
||||
<span class="nav-label">Usage</span>
|
||||
<span class="nav-indicator"></span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main content area -->
|
||||
<main class="app-main" role="main">
|
||||
<div class="content-wrapper">
|
||||
{{content}}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="app-footer">
|
||||
<p>OpenClaw Fleet Dashboard • <span class="footer-version">v2.0</span></p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{{markedScript}}
|
||||
{{chartScript}}
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
33
views/layout.html.backup
Normal file
33
views/layout.html.backup
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{pageTitle}}</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="top-bar">
|
||||
<h1 class="logo">🦞 OpenClaw Fleet Dashboard</h1>
|
||||
<div class="header-actions">
|
||||
<button id="theme-toggle" class="theme-btn" aria-label="Toggle theme">Light Mode</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="tab-nav">
|
||||
<a href="/tasks" class="tab-link {{tasksActive}}">📋 Tasks</a>
|
||||
<a href="/agents" class="tab-link {{agentsActive}}">🤖 Agents</a>
|
||||
<a href="/wiki" class="tab-link {{wikiActive}}">📚 Wiki</a>
|
||||
<a href="/gitea" class="tab-link {{giteaActive}}">🔧 Gitea</a>
|
||||
<a href="/usage" class="tab-link {{usageActive}}">📊 Usage</a>
|
||||
</nav>
|
||||
|
||||
<main class="main-content">
|
||||
{{content}}
|
||||
</main>
|
||||
|
||||
{{markedScript}}
|
||||
{{chartScript}}
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user