- 新增完整的 Python 实现,替代 Go 版本 - 添加 Web 登录界面和仪表板 - 实现 JWT 认证和 API 密钥管理 - 添加数据库存储功能 - 保持与 Go 版本一致的目录结构和启动脚本 - 包含完整的文档和测试脚本
373 lines
12 KiB
JavaScript
373 lines
12 KiB
JavaScript
// 全局变量存储 JWT 令牌
|
||
let authToken = localStorage.getItem('auth_token');
|
||
|
||
$(document).ready(function() {
|
||
// 检查认证状态
|
||
if (!authToken) {
|
||
window.location.href = '/login';
|
||
return;
|
||
}
|
||
|
||
// 设置 AJAX 默认配置
|
||
$.ajaxSetup({
|
||
beforeSend: function(xhr, settings) {
|
||
// 不为登录请求添加认证头
|
||
if (settings.url === '/api/auth/login') {
|
||
return;
|
||
}
|
||
if (authToken) {
|
||
xhr.setRequestHeader('Authorization', 'Bearer ' + authToken);
|
||
}
|
||
},
|
||
error: function(xhr, status, error) {
|
||
// 如果收到 401,重定向到登录页
|
||
if (xhr.status === 401) {
|
||
localStorage.removeItem('auth_token');
|
||
window.location.href = '/login';
|
||
return;
|
||
}
|
||
handleAjaxError(xhr, status, error);
|
||
}
|
||
});
|
||
|
||
// 初始化工具提示
|
||
$('[data-bs-toggle="tooltip"]').tooltip();
|
||
|
||
// 加载初始数据
|
||
loadProjects();
|
||
loadAPIKeys();
|
||
loadLogs();
|
||
checkHealth();
|
||
loadHealthDetails();
|
||
loadStatsDetails();
|
||
|
||
// 设置定期健康检查
|
||
setInterval(checkHealth, 30000);
|
||
|
||
// 项目管理
|
||
$('#addProjectForm').on('submit', function(e) {
|
||
e.preventDefault();
|
||
const projectData = {
|
||
name: $('#projectName').val(),
|
||
jenkinsJob: $('#jenkinsJob').val(),
|
||
giteaRepo: $('#giteaRepo').val()
|
||
};
|
||
|
||
$.ajax({
|
||
url: '/api/projects/',
|
||
method: 'POST',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify(projectData),
|
||
success: function() {
|
||
$('#addProjectModal').modal('hide');
|
||
$('#addProjectForm')[0].reset();
|
||
loadProjects();
|
||
showSuccess('项目添加成功');
|
||
},
|
||
error: handleAjaxError
|
||
});
|
||
});
|
||
|
||
// API 密钥管理
|
||
$('#generateKeyForm').on('submit', function(e) {
|
||
e.preventDefault();
|
||
$.ajax({
|
||
url: '/api/keys',
|
||
method: 'POST',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify({ description: $('#keyDescription').val() }),
|
||
success: function(response) {
|
||
$('#generateKeyModal').modal('hide');
|
||
$('#generateKeyForm')[0].reset();
|
||
loadAPIKeys();
|
||
showSuccess('API 密钥生成成功');
|
||
|
||
// 显示新生成的密钥
|
||
showApiKeyModal(response.key);
|
||
},
|
||
error: handleAjaxError
|
||
});
|
||
});
|
||
|
||
// 日志查询
|
||
$('#logQueryForm').on('submit', function(e) {
|
||
e.preventDefault();
|
||
loadLogs({
|
||
startTime: $('#startTime').val(),
|
||
endTime: $('#endTime').val(),
|
||
level: $('#logLevel').val(),
|
||
query: $('#logQuery').val()
|
||
});
|
||
});
|
||
|
||
// 标签页切换
|
||
$('.nav-link').on('click', function() {
|
||
$('.nav-link').removeClass('active');
|
||
$(this).addClass('active');
|
||
});
|
||
});
|
||
|
||
function loadProjects() {
|
||
$.get('/api/projects/')
|
||
.done(function(data) {
|
||
const tbody = $('#projectsTable tbody');
|
||
tbody.empty();
|
||
|
||
data.projects.forEach(function(project) {
|
||
tbody.append(`
|
||
<tr>
|
||
<td>${escapeHtml(project.name)}</td>
|
||
<td>${escapeHtml(project.jenkinsJob)}</td>
|
||
<td>${escapeHtml(project.giteaRepo)}</td>
|
||
<td>
|
||
<button class="btn btn-sm btn-danger" onclick="deleteProject(${project.id})">
|
||
<i class="bi bi-trash"></i> 删除
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`);
|
||
});
|
||
})
|
||
.fail(handleAjaxError);
|
||
}
|
||
|
||
function loadAPIKeys() {
|
||
$.get('/api/keys')
|
||
.done(function(data) {
|
||
const tbody = $('#apiKeysTable tbody');
|
||
tbody.empty();
|
||
|
||
data.keys.forEach(function(key) {
|
||
tbody.append(`
|
||
<tr>
|
||
<td>${escapeHtml(key.description || '无描述')}</td>
|
||
<td><code class="api-key">${escapeHtml(key.key)}</code></td>
|
||
<td>${new Date(key.created_at).toLocaleString('zh-CN')}</td>
|
||
<td>
|
||
<button class="btn btn-sm btn-danger" onclick="revokeKey(${key.id})">
|
||
<i class="bi bi-trash"></i> 撤销
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`);
|
||
});
|
||
})
|
||
.fail(handleAjaxError);
|
||
}
|
||
|
||
function loadLogs(query = {}) {
|
||
$.get('/api/logs', query)
|
||
.done(function(data) {
|
||
const logContainer = $('#logEntries');
|
||
logContainer.empty();
|
||
|
||
if (data.logs && data.logs.length > 0) {
|
||
data.logs.forEach(function(log) {
|
||
const levelClass = {
|
||
'error': 'error',
|
||
'warn': 'warn',
|
||
'info': 'info',
|
||
'debug': 'debug'
|
||
}[log.level] || '';
|
||
|
||
logContainer.append(`
|
||
<div class="log-entry ${levelClass}">
|
||
<small>${new Date(log.timestamp).toLocaleString('zh-CN')}</small>
|
||
[${escapeHtml(log.level.toUpperCase())}] ${escapeHtml(log.message)}
|
||
</div>
|
||
`);
|
||
});
|
||
} else {
|
||
logContainer.append('<div class="text-muted">暂无日志记录</div>');
|
||
}
|
||
})
|
||
.fail(handleAjaxError);
|
||
}
|
||
|
||
function checkHealth() {
|
||
$.get('/health')
|
||
.done(function(data) {
|
||
const indicator = $('.health-indicator');
|
||
indicator.removeClass('healthy unhealthy')
|
||
.addClass(data.status === 'healthy' ? 'healthy' : 'unhealthy');
|
||
$('#healthStatus').text(data.status === 'healthy' ? '健康' : '异常');
|
||
})
|
||
.fail(function() {
|
||
const indicator = $('.health-indicator');
|
||
indicator.removeClass('healthy').addClass('unhealthy');
|
||
$('#healthStatus').text('异常');
|
||
});
|
||
}
|
||
|
||
function loadHealthDetails() {
|
||
$.get('/health')
|
||
.done(function(data) {
|
||
const healthDetails = $('#healthDetails');
|
||
healthDetails.html(`
|
||
<div class="mb-3">
|
||
<strong>状态:</strong>
|
||
<span class="badge ${data.status === 'healthy' ? 'bg-success' : 'bg-danger'}">
|
||
${data.status === 'healthy' ? '健康' : '异常'}
|
||
</span>
|
||
</div>
|
||
<div class="mb-3">
|
||
<strong>版本:</strong> ${data.version || '未知'}
|
||
</div>
|
||
<div class="mb-3">
|
||
<strong>启动时间:</strong> ${data.uptime || '未知'}
|
||
</div>
|
||
<div class="mb-3">
|
||
<strong>内存使用:</strong> ${data.memory || '未知'}
|
||
</div>
|
||
`);
|
||
})
|
||
.fail(function() {
|
||
$('#healthDetails').html('<div class="text-danger">无法获取健康状态</div>');
|
||
});
|
||
}
|
||
|
||
function loadStatsDetails() {
|
||
$.get('/api/stats')
|
||
.done(function(data) {
|
||
const statsDetails = $('#statsDetails');
|
||
statsDetails.html(`
|
||
<div class="mb-3">
|
||
<strong>总项目数:</strong> ${data.total_projects || 0}
|
||
</div>
|
||
<div class="mb-3">
|
||
<strong>API 密钥数:</strong> ${data.total_api_keys || 0}
|
||
</div>
|
||
<div class="mb-3">
|
||
<strong>今日触发次数:</strong> ${data.today_triggers || 0}
|
||
</div>
|
||
<div class="mb-3">
|
||
<strong>成功触发次数:</strong> ${data.successful_triggers || 0}
|
||
</div>
|
||
`);
|
||
})
|
||
.fail(function() {
|
||
$('#statsDetails').html('<div class="text-danger">无法获取统计信息</div>');
|
||
});
|
||
}
|
||
|
||
function deleteProject(id) {
|
||
if (!confirm('确定要删除这个项目吗?')) return;
|
||
|
||
$.ajax({
|
||
url: `/api/projects/${id}`,
|
||
method: 'DELETE',
|
||
success: function() {
|
||
loadProjects();
|
||
showSuccess('项目删除成功');
|
||
},
|
||
error: handleAjaxError
|
||
});
|
||
}
|
||
|
||
function revokeKey(id) {
|
||
if (!confirm('确定要撤销这个 API 密钥吗?')) return;
|
||
|
||
$.ajax({
|
||
url: `/api/keys/${id}`,
|
||
method: 'DELETE',
|
||
success: function() {
|
||
loadAPIKeys();
|
||
showSuccess('API 密钥撤销成功');
|
||
},
|
||
error: handleAjaxError
|
||
});
|
||
}
|
||
|
||
function showApiKeyModal(key) {
|
||
// 创建模态框显示新生成的密钥
|
||
const modal = $(`
|
||
<div class="modal fade" id="newApiKeyModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">新 API 密钥</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="alert alert-warning">
|
||
<strong>重要提示:</strong> 请保存这个密钥,因为它只会显示一次!
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">API 密钥:</label>
|
||
<input type="text" class="form-control" value="${key}" readonly>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||
<button type="button" class="btn btn-primary" onclick="copyToClipboard('${key}')">
|
||
复制到剪贴板
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`);
|
||
|
||
$('body').append(modal);
|
||
modal.modal('show');
|
||
|
||
modal.on('hidden.bs.modal', function() {
|
||
modal.remove();
|
||
});
|
||
}
|
||
|
||
function copyToClipboard(text) {
|
||
navigator.clipboard.writeText(text).then(function() {
|
||
showSuccess('已复制到剪贴板');
|
||
}, function() {
|
||
showError('复制失败');
|
||
});
|
||
}
|
||
|
||
function handleAjaxError(jqXHR, textStatus, errorThrown) {
|
||
const message = jqXHR.responseJSON?.detail || errorThrown || '发生错误';
|
||
showError(`错误: ${message}`);
|
||
}
|
||
|
||
function showSuccess(message) {
|
||
// 创建成功提示
|
||
const alert = $(`
|
||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||
${message}
|
||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||
</div>
|
||
`);
|
||
|
||
$('.main-content').prepend(alert);
|
||
|
||
// 3秒后自动消失
|
||
setTimeout(function() {
|
||
alert.alert('close');
|
||
}, 3000);
|
||
}
|
||
|
||
function showError(message) {
|
||
// 创建错误提示
|
||
const alert = $(`
|
||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||
${message}
|
||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||
</div>
|
||
`);
|
||
|
||
$('.main-content').prepend(alert);
|
||
|
||
// 5秒后自动消失
|
||
setTimeout(function() {
|
||
alert.alert('close');
|
||
}, 5000);
|
||
}
|
||
|
||
function escapeHtml(unsafe) {
|
||
return unsafe
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|