fix logs, enable real-time status updates, and persist sessions
This commit is contained in:
13
control-plane/package-lock.json
generated
13
control-plane/package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"compression": "^1.7.4",
|
||||
"connect-pg-simple": "^9.0.1",
|
||||
"cors": "^2.8.5",
|
||||
"crypto": "^1.0.1",
|
||||
"dockerode": "^4.0.2",
|
||||
@@ -759,6 +760,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/connect-pg-simple": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-9.0.1.tgz",
|
||||
"integrity": "sha512-BuwWJH3K3aLpONkO9s12WhZ9ceMjIBxIJAh0JD9x4z1Y9nShmWqZvge5PG/+4j2cIOcguUoa2PSQ4HO/oTsrVg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg": "^8.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"connect-pg-simple": "^9.0.1",
|
||||
"pg": "^8.11.3",
|
||||
"dockerode": "^4.0.2",
|
||||
"ws": "^8.16.0",
|
||||
|
||||
@@ -55,6 +55,17 @@ router.post('/projects/:projectId/deploy', ensureAuthenticated, async (req, res,
|
||||
|
||||
const buildResult = await buildEngine.buildImage(deployment.id, buildPath, imageName);
|
||||
|
||||
// Update project port if a port was detected during build
|
||||
let projectPort = project.port;
|
||||
if (buildResult.detectedPort) {
|
||||
console.log(`[Deployment] Detected port ${buildResult.detectedPort} for project ${project.id}`);
|
||||
await db.query(
|
||||
'UPDATE projects SET port = $1 WHERE id = $2',
|
||||
[buildResult.detectedPort, project.id]
|
||||
);
|
||||
projectPort = buildResult.detectedPort;
|
||||
}
|
||||
|
||||
const envVars = await db.query(
|
||||
'SELECT key, value FROM env_vars WHERE project_id = $1',
|
||||
[project.id]
|
||||
@@ -84,7 +95,7 @@ router.post('/projects/:projectId/deploy', ensureAuthenticated, async (req, res,
|
||||
imageName,
|
||||
project.subdomain,
|
||||
envObject,
|
||||
project.port
|
||||
projectPort
|
||||
);
|
||||
|
||||
await fs.remove(buildPath);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const session = require('express-session');
|
||||
const pgSession = require('connect-pg-simple')(session);
|
||||
const db = require('./config/database');
|
||||
const passport = require('./config/github');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
@@ -18,6 +20,7 @@ const envVarsRoutes = require('./routes/envVars');
|
||||
const errorHandler = require('./middleware/errorHandler');
|
||||
const setupWebSocketServer = require('./websockets/logStreamer');
|
||||
const analyticsCollector = require('./services/analyticsCollector');
|
||||
const statusMonitor = require('./services/statusMonitor');
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
@@ -35,12 +38,17 @@ app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
app.use(session({
|
||||
store: new pgSession({
|
||||
pool: db.pool, // Use the existing database connection pool
|
||||
tableName: 'session', // Table will be created automatically
|
||||
createTableIfMissing: true
|
||||
}),
|
||||
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 24 * 60 * 60 * 1000
|
||||
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days instead of 1 day
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -69,6 +77,7 @@ app.use(errorHandler);
|
||||
setupWebSocketServer(server);
|
||||
|
||||
analyticsCollector.startStatsCollection();
|
||||
statusMonitor.start();
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ const path = require('path');
|
||||
|
||||
async function buildImage(deploymentId, repoPath, imageName) {
|
||||
try {
|
||||
await ensureDockerfile(repoPath);
|
||||
const dockerfileInfo = await ensureDockerfile(repoPath);
|
||||
console.log('[Build Engine] Dockerfile info:', dockerfileInfo);
|
||||
|
||||
await logBuild(deploymentId, 'Starting Docker build...');
|
||||
await updateDeploymentStatus(deploymentId, 'building');
|
||||
@@ -38,7 +39,7 @@ async function buildImage(deploymentId, repoPath, imageName) {
|
||||
);
|
||||
}
|
||||
|
||||
resolve({ imageId, imageName });
|
||||
resolve({ imageId, imageName, detectedPort: dockerfileInfo.detectedPort });
|
||||
}
|
||||
},
|
||||
async (event) => {
|
||||
|
||||
@@ -23,14 +23,19 @@ class LogAggregator {
|
||||
this.activeStreams.set(deploymentId, stream);
|
||||
|
||||
stream.on('data', async (chunk) => {
|
||||
const logLine = chunk.toString('utf8').trim();
|
||||
const logLine = this.sanitizeLogLine(chunk.toString('utf8')).trim();
|
||||
if (logLine) {
|
||||
const logLevel = this.detectLogLevel(logLine);
|
||||
|
||||
await db.query(
|
||||
'INSERT INTO runtime_logs (deployment_id, log_line, log_level) VALUES ($1, $2, $3)',
|
||||
[deploymentId, logLine, logLevel]
|
||||
);
|
||||
try {
|
||||
await db.query(
|
||||
'INSERT INTO runtime_logs (deployment_id, log_line, log_level) VALUES ($1, $2, $3)',
|
||||
[deploymentId, logLine, logLevel]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error inserting runtime log:', error.message);
|
||||
// Continue processing logs even if one fails
|
||||
}
|
||||
|
||||
if (onLog) {
|
||||
onLog(logLine);
|
||||
@@ -59,6 +64,15 @@ class LogAggregator {
|
||||
}
|
||||
}
|
||||
|
||||
sanitizeLogLine(logLine) {
|
||||
// Remove null bytes and other characters that PostgreSQL can't handle
|
||||
// Also strip ANSI color codes and control characters
|
||||
return logLine
|
||||
.replace(/\x00/g, '') // Remove null bytes
|
||||
.replace(/[\x00-\x09\x0B-\x0C\x0E-\x1F\x7F]/g, '') // Remove other control chars except \n and \r
|
||||
.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, ''); // Remove ANSI escape codes
|
||||
}
|
||||
|
||||
detectLogLevel(logLine) {
|
||||
const line = logLine.toLowerCase();
|
||||
|
||||
|
||||
84
control-plane/services/statusMonitor.js
Normal file
84
control-plane/services/statusMonitor.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const docker = require('../config/docker');
|
||||
const db = require('../config/database');
|
||||
|
||||
class StatusMonitor {
|
||||
constructor() {
|
||||
this.interval = null;
|
||||
}
|
||||
|
||||
start() {
|
||||
// Check status every 10 seconds
|
||||
this.interval = setInterval(() => this.checkAllStatuses(), 10000);
|
||||
console.log('Status monitor started');
|
||||
|
||||
// Run immediately on start
|
||||
this.checkAllStatuses();
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
this.interval = null;
|
||||
console.log('Status monitor stopped');
|
||||
}
|
||||
}
|
||||
|
||||
async checkAllStatuses() {
|
||||
try {
|
||||
// Get all deployments with docker containers
|
||||
const result = await db.query(
|
||||
`SELECT id, docker_container_id, status
|
||||
FROM deployments
|
||||
WHERE docker_container_id IS NOT NULL
|
||||
AND status IN ('running', 'building')`
|
||||
);
|
||||
|
||||
for (const deployment of result.rows) {
|
||||
await this.checkDeploymentStatus(deployment);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in status monitor:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async checkDeploymentStatus(deployment) {
|
||||
try {
|
||||
const container = docker.getContainer(deployment.docker_container_id);
|
||||
const info = await container.inspect();
|
||||
|
||||
let newStatus = deployment.status;
|
||||
|
||||
if (info.State.Running) {
|
||||
newStatus = 'running';
|
||||
} else if (info.State.Status === 'exited') {
|
||||
newStatus = 'stopped';
|
||||
} else if (info.State.Status === 'dead') {
|
||||
newStatus = 'failed';
|
||||
} else if (info.State.Restarting) {
|
||||
newStatus = 'restarting';
|
||||
}
|
||||
|
||||
// Update status if it changed
|
||||
if (newStatus !== deployment.status) {
|
||||
console.log(`Deployment ${deployment.id}: ${deployment.status} → ${newStatus}`);
|
||||
await db.query(
|
||||
'UPDATE deployments SET status = $1, updated_at = NOW() WHERE id = $2',
|
||||
[newStatus, deployment.id]
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Container not found or other error - mark as stopped
|
||||
if (error.statusCode === 404) {
|
||||
console.log(`Deployment ${deployment.id}: Container not found, marking as stopped`);
|
||||
await db.query(
|
||||
'UPDATE deployments SET status = $1, updated_at = NOW() WHERE id = $2',
|
||||
['stopped', deployment.id]
|
||||
);
|
||||
} else {
|
||||
console.error(`Error checking deployment ${deployment.id}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new StatusMonitor();
|
||||
@@ -1,6 +1,94 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Detect Flask application entry point by checking common filenames
|
||||
* and scanning for Flask app instantiation
|
||||
*/
|
||||
function detectFlaskEntryPoint(repoPath) {
|
||||
console.log('[Flask Detection] Scanning repo path:', repoPath);
|
||||
|
||||
// Common Flask entry point filenames in order of preference
|
||||
const commonEntryPoints = [
|
||||
'app.py',
|
||||
'main.py',
|
||||
'application.py',
|
||||
'run.py',
|
||||
'wsgi.py',
|
||||
'server.py',
|
||||
'__init__.py'
|
||||
];
|
||||
|
||||
// First, check if any of the common entry points exist
|
||||
const existingEntryPoints = commonEntryPoints.filter(filename => {
|
||||
const exists = fs.existsSync(path.join(repoPath, filename));
|
||||
if (exists) {
|
||||
console.log('[Flask Detection] Found:', filename);
|
||||
}
|
||||
return exists;
|
||||
});
|
||||
|
||||
console.log('[Flask Detection] Existing entry points:', existingEntryPoints);
|
||||
|
||||
if (existingEntryPoints.length === 0) {
|
||||
// No common entry points found, scan all Python files
|
||||
console.log('[Flask Detection] No common entry points, scanning all .py files');
|
||||
try {
|
||||
const files = fs.readdirSync(repoPath);
|
||||
const pythonFiles = files.filter(f => f.endsWith('.py'));
|
||||
console.log('[Flask Detection] Python files found:', pythonFiles);
|
||||
|
||||
for (const file of pythonFiles) {
|
||||
const content = fs.readFileSync(path.join(repoPath, file), 'utf8');
|
||||
// Check if file contains Flask app instantiation
|
||||
if (content.includes('Flask(__name__)') || content.includes('Flask(')) {
|
||||
console.log('[Flask Detection] Found Flask app in:', file);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('[Flask Detection] Error scanning files:', err.message);
|
||||
// If scanning fails, use default
|
||||
return 'app.py';
|
||||
}
|
||||
// No Flask app found, use default
|
||||
console.log('[Flask Detection] No Flask app found, defaulting to app.py');
|
||||
return 'app.py';
|
||||
}
|
||||
|
||||
// If only one common entry point exists, use it
|
||||
if (existingEntryPoints.length === 1) {
|
||||
console.log('[Flask Detection] Using single entry point:', existingEntryPoints[0]);
|
||||
return existingEntryPoints[0];
|
||||
}
|
||||
|
||||
// Multiple entry points exist, scan them to find which one has Flask app
|
||||
console.log('[Flask Detection] Multiple entry points, scanning for Flask app');
|
||||
for (const file of existingEntryPoints) {
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(repoPath, file), 'utf8');
|
||||
// Look for Flask app instantiation patterns
|
||||
if (
|
||||
content.includes('Flask(__name__)') ||
|
||||
content.includes('Flask(') ||
|
||||
content.includes('create_app') ||
|
||||
content.includes('app = ') ||
|
||||
content.includes('application = ')
|
||||
) {
|
||||
console.log('[Flask Detection] Found Flask app in:', file);
|
||||
return file;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('[Flask Detection] Error reading', file, ':', err.message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Default to the first common entry point found
|
||||
console.log('[Flask Detection] Defaulting to first entry point:', existingEntryPoints[0]);
|
||||
return existingEntryPoints[0];
|
||||
}
|
||||
|
||||
function detectProjectType(repoPath) {
|
||||
if (fs.existsSync(path.join(repoPath, 'Dockerfile'))) {
|
||||
return 'existing';
|
||||
@@ -22,6 +110,7 @@ function detectProjectType(repoPath) {
|
||||
|
||||
function generateDockerfile(repoPath, projectType) {
|
||||
let dockerfile = '';
|
||||
let detectedPort = null; // Track the detected port
|
||||
|
||||
switch (projectType) {
|
||||
case 'nodejs':
|
||||
@@ -84,16 +173,33 @@ CMD ["${hasYarnLock ? 'yarn' : 'npm'}", "start"]
|
||||
case 'python':
|
||||
// Check if Flask/FastAPI exists in requirements
|
||||
let cmd = '["python", "app.py"]';
|
||||
let flaskApp = 'app.py';
|
||||
let envVars = '';
|
||||
let port = 8000; // Default Python app port
|
||||
|
||||
const requirementsPath = path.join(repoPath, 'requirements.txt');
|
||||
if (fs.existsSync(requirementsPath)) {
|
||||
const requirements = fs.readFileSync(requirementsPath, 'utf8').toLowerCase();
|
||||
if (requirements.includes('flask')) {
|
||||
cmd = '["python", "-m", "flask", "run", "--host=0.0.0.0"]';
|
||||
// Dynamically detect Flask entry point
|
||||
flaskApp = detectFlaskEntryPoint(repoPath);
|
||||
port = 5000; // Flask default port
|
||||
cmd = '["python", "-m", "flask", "run", "--host=0.0.0.0", "--port=5000"]';
|
||||
envVars = `ENV FLASK_APP=${flaskApp}
|
||||
ENV FLASK_RUN_HOST=0.0.0.0
|
||||
ENV FLASK_RUN_PORT=5000
|
||||
`;
|
||||
} else if (requirements.includes('fastapi') || requirements.includes('uvicorn')) {
|
||||
cmd = '["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]';
|
||||
// For FastAPI, try to detect the entry point too
|
||||
const entryPoint = detectFlaskEntryPoint(repoPath); // Reuse function for detection
|
||||
const moduleName = entryPoint.replace('.py', '');
|
||||
port = 8000; // FastAPI/Uvicorn default port
|
||||
cmd = `["uvicorn", "${moduleName}:app", "--host", "0.0.0.0", "--port", "8000"]`;
|
||||
}
|
||||
}
|
||||
|
||||
detectedPort = port; // Set the detected port
|
||||
|
||||
dockerfile = `FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
@@ -103,10 +209,8 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8000
|
||||
ENV FLASK_APP=app.py
|
||||
ENV FLASK_RUN_HOST=0.0.0.0
|
||||
|
||||
EXPOSE ${port}
|
||||
${envVars}
|
||||
CMD ${cmd}
|
||||
`;
|
||||
break;
|
||||
@@ -147,7 +251,7 @@ CMD ["nginx", "-g", "daemon off;"]
|
||||
throw new Error('Unknown project type - cannot generate Dockerfile');
|
||||
}
|
||||
|
||||
return dockerfile;
|
||||
return { dockerfile, detectedPort };
|
||||
}
|
||||
|
||||
function ensureDockerfile(repoPath) {
|
||||
@@ -163,10 +267,10 @@ function ensureDockerfile(repoPath) {
|
||||
throw new Error('Cannot detect project type');
|
||||
}
|
||||
|
||||
const dockerfile = generateDockerfile(repoPath, projectType);
|
||||
const { dockerfile, detectedPort } = generateDockerfile(repoPath, projectType);
|
||||
fs.writeFileSync(dockerfilePath, dockerfile);
|
||||
|
||||
return { exists: false, generated: true, projectType };
|
||||
return { exists: false, generated: true, projectType, detectedPort };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -7,7 +7,38 @@
|
||||
|
||||
.log-filters {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.log-type-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background-color: var(--bg-tertiary);
|
||||
padding: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.log-tab {
|
||||
padding: 6px 16px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.log-tab:hover {
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.log-tab.active {
|
||||
background-color: var(--accent-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.log-filter-btn {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
let currentProject = null;
|
||||
let projectDetailRefreshInterval = null;
|
||||
|
||||
async function openProjectDetail(projectId) {
|
||||
try {
|
||||
@@ -8,6 +9,9 @@ async function openProjectDetail(projectId) {
|
||||
|
||||
switchTab('overview');
|
||||
renderOverviewTab();
|
||||
|
||||
// Start auto-refresh for project details
|
||||
startProjectDetailAutoRefresh();
|
||||
} catch (error) {
|
||||
console.error('Error loading project:', error);
|
||||
showNotification('Failed to load project details', 'error');
|
||||
@@ -16,9 +20,46 @@ async function openProjectDetail(projectId) {
|
||||
|
||||
function closeProjectDetail() {
|
||||
document.getElementById('projectDetailModal').classList.remove('active');
|
||||
stopProjectDetailAutoRefresh();
|
||||
currentProject = null;
|
||||
}
|
||||
|
||||
async function refreshProjectDetail() {
|
||||
if (!currentProject) return;
|
||||
|
||||
try {
|
||||
const updated = await api.get(`/api/projects/${currentProject.id}`);
|
||||
currentProject = updated;
|
||||
|
||||
// Re-render the current tab
|
||||
const activeTab = document.querySelector('.tab-button.active');
|
||||
if (activeTab) {
|
||||
const tabName = activeTab.textContent.toLowerCase();
|
||||
switchTab(tabName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing project:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function startProjectDetailAutoRefresh() {
|
||||
if (projectDetailRefreshInterval) {
|
||||
clearInterval(projectDetailRefreshInterval);
|
||||
}
|
||||
|
||||
// Refresh every 3 seconds
|
||||
projectDetailRefreshInterval = setInterval(() => {
|
||||
refreshProjectDetail();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function stopProjectDetailAutoRefresh() {
|
||||
if (projectDetailRefreshInterval) {
|
||||
clearInterval(projectDetailRefreshInterval);
|
||||
projectDetailRefreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(tabName) {
|
||||
document.querySelectorAll('.tab-button').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
let logWebSocket = null;
|
||||
let currentDeploymentId = null;
|
||||
let currentLogType = 'all'; // 'all', 'build', or 'runtime'
|
||||
|
||||
function renderLogsTab() {
|
||||
const tab = document.getElementById('logsTab');
|
||||
@@ -16,6 +17,11 @@ function renderLogsTab() {
|
||||
</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
<div class="log-type-tabs">
|
||||
<button class="log-tab active" data-type="all" onclick="switchLogType('all')">All</button>
|
||||
<button class="log-tab" data-type="build" onclick="switchLogType('build')">Build</button>
|
||||
<button class="log-tab" data-type="runtime" onclick="switchLogType('runtime')">Runtime</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-search">
|
||||
<button class="btn btn-secondary btn-icon" onclick="clearLogs()" title="Clear">
|
||||
@@ -100,12 +106,20 @@ function appendLog(logLine, level = 'info', source = 'runtime') {
|
||||
const logViewer = document.getElementById('logViewer');
|
||||
if (!logViewer) return;
|
||||
|
||||
// Filter logs based on current log type
|
||||
if (currentLogType !== 'all' && source !== currentLogType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logElement = document.createElement('div');
|
||||
logElement.className = `log-line log-${level}`;
|
||||
logElement.dataset.source = source;
|
||||
|
||||
const cleanedLog = logLine.replace(/[\x00-\x1F\x7F-\x9F]/g, '').trim();
|
||||
|
||||
logElement.textContent = cleanedLog;
|
||||
// Add source badge
|
||||
const sourceBadge = source === 'build' ? '[BUILD] ' : '[RUNTIME] ';
|
||||
logElement.textContent = sourceBadge + cleanedLog;
|
||||
|
||||
logViewer.appendChild(logElement);
|
||||
|
||||
@@ -116,6 +130,32 @@ function appendLog(logLine, level = 'info', source = 'runtime') {
|
||||
logViewer.scrollTop = logViewer.scrollHeight;
|
||||
}
|
||||
|
||||
function switchLogType(type) {
|
||||
currentLogType = type;
|
||||
|
||||
// Update active tab styling
|
||||
document.querySelectorAll('.log-tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
if (tab.dataset.type === type) {
|
||||
tab.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Filter existing logs
|
||||
const logViewer = document.getElementById('logViewer');
|
||||
if (!logViewer) return;
|
||||
|
||||
const logLines = logViewer.querySelectorAll('.log-line');
|
||||
logLines.forEach(logLine => {
|
||||
const source = logLine.dataset.source;
|
||||
if (type === 'all' || source === type || !source) {
|
||||
logLine.style.display = '';
|
||||
} else {
|
||||
logLine.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
const logViewer = document.getElementById('logViewer');
|
||||
if (logViewer) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
let currentProjects = [];
|
||||
let repositories = [];
|
||||
let projectsRefreshInterval = null;
|
||||
|
||||
async function loadProjects() {
|
||||
try {
|
||||
@@ -17,6 +18,25 @@ async function loadProjects() {
|
||||
}
|
||||
}
|
||||
|
||||
function startProjectsAutoRefresh() {
|
||||
// Clear any existing interval
|
||||
if (projectsRefreshInterval) {
|
||||
clearInterval(projectsRefreshInterval);
|
||||
}
|
||||
|
||||
// Refresh every 5 seconds
|
||||
projectsRefreshInterval = setInterval(() => {
|
||||
loadProjects();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function stopProjectsAutoRefresh() {
|
||||
if (projectsRefreshInterval) {
|
||||
clearInterval(projectsRefreshInterval);
|
||||
projectsRefreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function renderProjects(projects) {
|
||||
const grid = document.getElementById('projectsGrid');
|
||||
|
||||
@@ -119,4 +139,5 @@ async function createProject(event) {
|
||||
|
||||
if (window.location.pathname === '/' || window.location.pathname === '/index.html') {
|
||||
loadProjects();
|
||||
startProjectsAutoRefresh();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
- "--api.insecure=true"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--providers.docker.network=paas_network"
|
||||
- "--providers.docker.network=1minipaas_paas_network"
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--accesslog=true"
|
||||
- "--accesslog.filepath=/var/log/traefik/access.log"
|
||||
|
||||
@@ -10,7 +10,7 @@ providers:
|
||||
docker:
|
||||
endpoint: "unix:///var/run/docker.sock"
|
||||
exposedByDefault: false
|
||||
network: "paas_network"
|
||||
network: "1minipaas_paas_network"
|
||||
|
||||
accessLog:
|
||||
filePath: "/var/log/traefik/access.log"
|
||||
|
||||
Reference in New Issue
Block a user