coolify integration.
This commit is contained in:
922
app/renderer-legacy/app.js
Normal file
922
app/renderer-legacy/app.js
Normal file
@@ -0,0 +1,922 @@
|
||||
// State
|
||||
let state = {
|
||||
servers: [],
|
||||
selectedServerId: null,
|
||||
localProjects: [],
|
||||
deployedProjects: [],
|
||||
runningContainers: [],
|
||||
currentDiffProject: null,
|
||||
currentDetailsProject: null,
|
||||
currentDeployProject: null,
|
||||
currentDeployDiff: null,
|
||||
currentLogsProject: null
|
||||
};
|
||||
|
||||
// DOM Elements
|
||||
const serverSelect = document.getElementById('server-select');
|
||||
const btnScanServer = document.getElementById('btn-scan-server');
|
||||
const serverStatus = document.getElementById('server-status');
|
||||
const projectsBody = document.getElementById('projects-body');
|
||||
const btnRefresh = document.getElementById('btn-refresh');
|
||||
const btnSettings = document.getElementById('btn-settings');
|
||||
|
||||
// Modals
|
||||
const serverModal = document.getElementById('server-modal');
|
||||
const diffModal = document.getElementById('diff-modal');
|
||||
const logModal = document.getElementById('log-modal');
|
||||
const detailsModal = document.getElementById('details-modal');
|
||||
const deployConfirmModal = document.getElementById('deploy-confirm-modal');
|
||||
const logsModal = document.getElementById('logs-modal');
|
||||
|
||||
// Initialize
|
||||
async function init() {
|
||||
await loadServers();
|
||||
await loadLocalProjects();
|
||||
setupEventListeners();
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
btnRefresh.addEventListener('click', () => loadLocalProjects());
|
||||
btnSettings.addEventListener('click', () => showServerModal());
|
||||
btnScanServer.addEventListener('click', () => scanServer());
|
||||
|
||||
serverSelect.addEventListener('change', (e) => {
|
||||
state.selectedServerId = e.target.value || null;
|
||||
btnScanServer.disabled = !state.selectedServerId;
|
||||
state.deployedProjects = [];
|
||||
state.runningContainers = [];
|
||||
renderProjects();
|
||||
});
|
||||
|
||||
// Modal close buttons
|
||||
document.getElementById('close-server-modal').addEventListener('click', () => hideModal(serverModal));
|
||||
document.getElementById('close-diff-modal').addEventListener('click', () => hideModal(diffModal));
|
||||
document.getElementById('close-log-modal').addEventListener('click', () => hideModal(logModal));
|
||||
document.getElementById('close-details-modal').addEventListener('click', () => hideModal(detailsModal));
|
||||
document.getElementById('close-deploy-confirm').addEventListener('click', () => hideModal(deployConfirmModal));
|
||||
document.getElementById('close-logs-modal').addEventListener('click', () => hideModal(logsModal));
|
||||
|
||||
// Server form
|
||||
document.getElementById('btn-save-server').addEventListener('click', saveServer);
|
||||
|
||||
// Details modal - init project button
|
||||
document.getElementById('btn-init-project').addEventListener('click', initCurrentProject);
|
||||
|
||||
// Deploy confirmation modal buttons
|
||||
document.getElementById('btn-deploy-continue').addEventListener('click', executeDeployment);
|
||||
document.getElementById('btn-deploy-abort').addEventListener('click', () => hideModal(deployConfirmModal));
|
||||
document.getElementById('btn-deploy-vscode').addEventListener('click', openVSCodeDiff);
|
||||
|
||||
// Logs modal - refresh button
|
||||
document.getElementById('btn-refresh-logs').addEventListener('click', refreshLogs);
|
||||
}
|
||||
|
||||
// Load servers
|
||||
async function loadServers() {
|
||||
state.servers = await window.api.getServers();
|
||||
renderServerSelect();
|
||||
}
|
||||
|
||||
function renderServerSelect() {
|
||||
serverSelect.innerHTML = '<option value="">-- Select Server --</option>';
|
||||
for (const server of state.servers) {
|
||||
const option = document.createElement('option');
|
||||
option.value = server.id;
|
||||
option.textContent = `${server.name} (${server.host})`;
|
||||
serverSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
// Load local projects
|
||||
async function loadLocalProjects() {
|
||||
projectsBody.innerHTML = '<tr><td colspan="6" class="loading">Scanning local projects...</td></tr>';
|
||||
|
||||
try {
|
||||
state.localProjects = await window.api.scanLocalProjects();
|
||||
renderProjects();
|
||||
} catch (err) {
|
||||
projectsBody.innerHTML = `<tr><td colspan="6" class="loading">Error: ${err.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Scan remote server
|
||||
async function scanServer() {
|
||||
if (!state.selectedServerId) return;
|
||||
|
||||
serverStatus.textContent = 'Connecting...';
|
||||
serverStatus.className = 'status-badge';
|
||||
|
||||
try {
|
||||
// Get deployed projects
|
||||
const deployedResult = await window.api.scanServer(state.selectedServerId);
|
||||
if (deployedResult.error) {
|
||||
throw new Error(deployedResult.error);
|
||||
}
|
||||
state.deployedProjects = deployedResult.deployed || [];
|
||||
|
||||
// Get running containers
|
||||
const containersResult = await window.api.getRunningContainers(state.selectedServerId);
|
||||
if (containersResult.success) {
|
||||
state.runningContainers = containersResult.containers || [];
|
||||
}
|
||||
|
||||
serverStatus.textContent = `Connected (${state.deployedProjects.length} deployed)`;
|
||||
serverStatus.className = 'status-badge connected';
|
||||
renderProjects();
|
||||
} catch (err) {
|
||||
serverStatus.textContent = `Error: ${err.message}`;
|
||||
serverStatus.className = 'status-badge error';
|
||||
}
|
||||
}
|
||||
|
||||
// Render projects table
|
||||
function renderProjects() {
|
||||
if (state.localProjects.length === 0) {
|
||||
projectsBody.innerHTML = '<tr><td colspan="6" class="loading">No projects found with Dockerfiles</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Merge local and deployed data
|
||||
const projectMap = new Map();
|
||||
|
||||
// Add local projects
|
||||
for (const project of state.localProjects) {
|
||||
projectMap.set(project.name, {
|
||||
...project,
|
||||
deployed: null,
|
||||
running: null
|
||||
});
|
||||
}
|
||||
|
||||
// Match with deployed projects
|
||||
for (const deployed of state.deployedProjects) {
|
||||
if (projectMap.has(deployed.name)) {
|
||||
projectMap.get(deployed.name).deployed = deployed;
|
||||
} else {
|
||||
// Project exists on server but not locally
|
||||
projectMap.set(deployed.name, {
|
||||
name: deployed.name,
|
||||
path: null,
|
||||
hasDockerfile: false,
|
||||
hasDockerCompose: false,
|
||||
dockerStatus: 'remote-only',
|
||||
deployed
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Match with running containers
|
||||
for (const container of state.runningContainers) {
|
||||
// Try to match container name with project name
|
||||
for (const [name, project] of projectMap) {
|
||||
if (container.name.includes(name) || name.includes(container.name)) {
|
||||
project.running = container;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render table rows
|
||||
projectsBody.innerHTML = '';
|
||||
|
||||
for (const [name, project] of projectMap) {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>
|
||||
<span class="project-name">${name}</span>
|
||||
${project.path ? `<span class="project-path">${project.path}</span>` : ''}
|
||||
</td>
|
||||
<td class="status-cell">
|
||||
${renderLocalStatus(project)}
|
||||
</td>
|
||||
<td class="status-cell">
|
||||
${renderDeployedStatus(project)}
|
||||
</td>
|
||||
<td class="status-cell">
|
||||
${renderRunningStatus(project)}
|
||||
</td>
|
||||
<td>
|
||||
${renderDiffStatus(project)}
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
${renderActions(project)}
|
||||
</td>
|
||||
`;
|
||||
projectsBody.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
function renderLocalStatus(project) {
|
||||
if (!project.path) {
|
||||
return '<span class="indicator gray"></span>Not local';
|
||||
}
|
||||
|
||||
// Count missing files
|
||||
const requiredFiles = getRequiredFiles(project);
|
||||
const missingCount = requiredFiles.filter(f => !f.present).length;
|
||||
|
||||
if (missingCount === 0) {
|
||||
return `<span class="issues-badge ok" onclick="showDetails('${project.name}')">Ready</span>`;
|
||||
} else if (missingCount <= 2) {
|
||||
return `<span class="issues-badge warning" onclick="showDetails('${project.name}')">${missingCount} missing</span>`;
|
||||
} else {
|
||||
return `<span class="issues-badge error" onclick="showDetails('${project.name}')">${missingCount} missing</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderDeployedStatus(project) {
|
||||
if (!state.selectedServerId) {
|
||||
return '<span class="indicator gray"></span>Select server';
|
||||
}
|
||||
|
||||
if (project.deployed) {
|
||||
return '<span class="indicator green"></span>Deployed';
|
||||
} else {
|
||||
return '<span class="indicator gray"></span>Not deployed';
|
||||
}
|
||||
}
|
||||
|
||||
function renderRunningStatus(project) {
|
||||
if (!state.selectedServerId) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
if (project.running) {
|
||||
return `<span class="indicator green"></span>${project.running.status.split(' ')[0]}`;
|
||||
} else if (project.deployed) {
|
||||
return '<span class="indicator red"></span>Stopped';
|
||||
} else {
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
|
||||
function renderDiffStatus(project) {
|
||||
if (!project.deployed || !project.path) {
|
||||
return '<span class="diff-badge unknown">N/A</span>';
|
||||
}
|
||||
|
||||
// If we have deployed content, we can compare
|
||||
if (project.deployed.dockerComposeContent && project.hasDockerCompose) {
|
||||
return `<button class="btn btn-small btn-secondary" onclick="showDiff('${project.name}')">Compare</button>`;
|
||||
}
|
||||
|
||||
return '<span class="diff-badge unknown">?</span>';
|
||||
}
|
||||
|
||||
function renderActions(project) {
|
||||
const actions = [];
|
||||
|
||||
if (project.path && project.hasDockerfile) {
|
||||
actions.push(`<button class="btn btn-small btn-secondary" onclick="buildTar('${project.path}')">Build</button>`);
|
||||
}
|
||||
|
||||
if (project.path && state.selectedServerId) {
|
||||
actions.push(`<button class="btn btn-small btn-primary" onclick="deployProject('${project.name}')">Deploy</button>`);
|
||||
}
|
||||
|
||||
// Add logs button for running containers
|
||||
if (project.running && state.selectedServerId) {
|
||||
actions.push(`<button class="btn btn-small btn-secondary" onclick="viewLogs('${project.name}')">Logs</button>`);
|
||||
}
|
||||
|
||||
return actions.join(' ');
|
||||
}
|
||||
|
||||
// Actions
|
||||
async function buildTar(projectPath) {
|
||||
showLog('Building Docker image and tar...\n');
|
||||
|
||||
const result = await window.api.buildTar(projectPath);
|
||||
|
||||
if (result.error) {
|
||||
appendLog(`\nError: ${result.error}\n${result.stderr || ''}`);
|
||||
} else {
|
||||
appendLog(result.output);
|
||||
appendLog('\n\nBuild complete!');
|
||||
}
|
||||
}
|
||||
|
||||
async function deployProject(projectName) {
|
||||
const project = state.localProjects.find(p => p.name === projectName);
|
||||
if (!project || !state.selectedServerId) return;
|
||||
|
||||
state.currentDeployProject = project;
|
||||
|
||||
// Step 1: Compare files first
|
||||
showLog(`Checking for differences before deploy...`);
|
||||
|
||||
const comparison = await window.api.compareProject({
|
||||
projectPath: project.path,
|
||||
serverId: state.selectedServerId,
|
||||
remotePath: project.remotePath || `~/containers/${projectName}`
|
||||
});
|
||||
|
||||
hideModal(logModal);
|
||||
|
||||
if (comparison.error) {
|
||||
showLog(`Error comparing files: ${comparison.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
state.currentDeployDiff = comparison.diff;
|
||||
|
||||
// Step 2: Check if there are differences
|
||||
const composeStatus = comparison.diff.dockerCompose.status;
|
||||
const envStatus = comparison.diff.env.status;
|
||||
const hasDiff = composeStatus === 'different' || envStatus === 'different';
|
||||
const isNewDeploy = composeStatus === 'remote-missing' && envStatus !== 'different';
|
||||
|
||||
if (isNewDeploy) {
|
||||
// New deployment - no confirmation needed
|
||||
await executeDeployment();
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasDiff) {
|
||||
// Show confirmation modal with differences
|
||||
showDeployConfirmModal(project, comparison.diff);
|
||||
return;
|
||||
}
|
||||
|
||||
// No differences - proceed directly
|
||||
await executeDeployment();
|
||||
}
|
||||
|
||||
function showDeployConfirmModal(project, diff) {
|
||||
document.getElementById('deploy-confirm-project').textContent = project.name;
|
||||
|
||||
// Check all files for differences
|
||||
const filesWithDiff = (diff.files || []).filter(f =>
|
||||
f.status === 'different' || f.status === 'local-missing'
|
||||
);
|
||||
const hasDiff = filesWithDiff.length > 0;
|
||||
|
||||
// Summary
|
||||
const summaryEl = document.getElementById('deploy-diff-summary');
|
||||
if (hasDiff) {
|
||||
summaryEl.className = 'deploy-diff-summary has-diff';
|
||||
summaryEl.innerHTML = `
|
||||
<strong>Differences found!</strong> ${filesWithDiff.length} file(s) differ from deployed versions.
|
||||
<br>Continuing will overwrite the remote files.
|
||||
${filesWithDiff.some(f => f.status === 'local-missing') ?
|
||||
'<br><span style="color: #f39c12;">Warning: Some files exist on server but not locally.</span>' : ''}
|
||||
`;
|
||||
} else {
|
||||
summaryEl.className = 'deploy-diff-summary no-diff';
|
||||
summaryEl.innerHTML = `<strong>No differences found.</strong> Local and remote files match.`;
|
||||
}
|
||||
|
||||
// Details - show all files with their status
|
||||
const detailsEl = document.getElementById('deploy-diff-details');
|
||||
detailsEl.innerHTML = '';
|
||||
|
||||
// Show all compared files
|
||||
const files = diff.files || [];
|
||||
for (const file of files) {
|
||||
const statusBadge = getStatusBadge(file.status);
|
||||
const fileIndex = files.indexOf(file);
|
||||
|
||||
if (file.status === 'different') {
|
||||
// Show diff for files that are different
|
||||
const content = file.sensitive ?
|
||||
{ local: maskEnvContent(file.localContent), remote: maskEnvContent(file.remoteContent) } :
|
||||
{ local: file.localContent, remote: file.remoteContent };
|
||||
|
||||
detailsEl.innerHTML += `
|
||||
<div class="deploy-file-diff">
|
||||
<h4>
|
||||
${file.name} ${statusBadge}
|
||||
<button class="btn btn-small btn-secondary" onclick="pullSingleFile(${fileIndex})" title="Pull from server">Pull</button>
|
||||
</h4>
|
||||
<div class="diff-content">
|
||||
<div class="diff-side">
|
||||
<div class="diff-side-header">Local</div>
|
||||
<pre>${escapeHtml(content.local || '(empty)')}</pre>
|
||||
</div>
|
||||
<div class="diff-side">
|
||||
<div class="diff-side-header">Remote</div>
|
||||
<pre>${escapeHtml(content.remote || '(empty)')}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (file.status === 'local-missing' && file.remoteContent) {
|
||||
// File exists on server but not locally
|
||||
detailsEl.innerHTML += `
|
||||
<div class="deploy-file-diff">
|
||||
<h4>
|
||||
${file.name} ${statusBadge}
|
||||
<button class="btn btn-small btn-secondary" onclick="pullSingleFile(${fileIndex})" title="Pull from server">Pull</button>
|
||||
</h4>
|
||||
<div class="diff-content">
|
||||
<div class="diff-side">
|
||||
<div class="diff-side-header">Local</div>
|
||||
<pre>(not found)</pre>
|
||||
</div>
|
||||
<div class="diff-side">
|
||||
<div class="diff-side-header">Remote</div>
|
||||
<pre>${escapeHtml(file.sensitive ? maskEnvContent(file.remoteContent) : file.remoteContent)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (file.type === 'directory' && file.status !== 'neither') {
|
||||
// Directory status
|
||||
detailsEl.innerHTML += `
|
||||
<div class="deploy-file-diff" style="padding: 12px;">
|
||||
<h4 style="margin: 0;">
|
||||
${file.name} ${statusBadge}
|
||||
${file.status === 'local-missing' || file.status === 'both-exist' ?
|
||||
`<button class="btn btn-small btn-secondary" onclick="pullSingleFile(${fileIndex})" title="Pull from server">Pull</button>` : ''}
|
||||
</h4>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add "Pull All Different" button if there are pullable files
|
||||
const pullableFiles = files.filter(f =>
|
||||
(f.status === 'different' || f.status === 'local-missing') && f.remoteContent
|
||||
);
|
||||
if (pullableFiles.length > 0) {
|
||||
detailsEl.innerHTML += `
|
||||
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #0f3460;">
|
||||
<button class="btn btn-secondary" onclick="pullAllDifferent()">
|
||||
Pull All Different Files (${pullableFiles.length})
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
showModal(deployConfirmModal);
|
||||
}
|
||||
|
||||
function getStatusBadge(status) {
|
||||
switch (status) {
|
||||
case 'match':
|
||||
return '<span class="diff-badge match">Match</span>';
|
||||
case 'different':
|
||||
return '<span class="diff-badge different">Different</span>';
|
||||
case 'remote-missing':
|
||||
return '<span class="diff-badge missing">Not on server</span>';
|
||||
case 'local-missing':
|
||||
return '<span class="diff-badge warning" style="background: #f39c12; color: #000;">Only on server</span>';
|
||||
case 'both-exist':
|
||||
return '<span class="diff-badge match">Both exist</span>';
|
||||
case 'neither':
|
||||
return '<span class="diff-badge unknown">Neither</span>';
|
||||
default:
|
||||
return '<span class="diff-badge unknown">Unknown</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Pull a single file from server
|
||||
window.pullSingleFile = async function(fileIndex) {
|
||||
const diff = state.currentDeployDiff;
|
||||
const project = state.currentDeployProject;
|
||||
if (!diff || !project || !diff.files[fileIndex]) return;
|
||||
|
||||
const file = diff.files[fileIndex];
|
||||
showLog(`Pulling ${file.name} from server...`);
|
||||
|
||||
const result = await window.api.pullFile({
|
||||
serverId: state.selectedServerId,
|
||||
remotePath: file.remotePath,
|
||||
localPath: file.localPath,
|
||||
isDirectory: file.type === 'directory'
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
appendLog(`\nError: ${result.error}`);
|
||||
} else {
|
||||
appendLog(`\nPulled ${file.name} successfully!`);
|
||||
// Refresh comparison
|
||||
hideModal(logModal);
|
||||
await deployProject(project.name);
|
||||
}
|
||||
};
|
||||
|
||||
// Pull all different files from server
|
||||
window.pullAllDifferent = async function() {
|
||||
const diff = state.currentDeployDiff;
|
||||
const project = state.currentDeployProject;
|
||||
if (!diff || !project) return;
|
||||
|
||||
const filesToPull = (diff.files || []).filter(f =>
|
||||
(f.status === 'different' || f.status === 'local-missing') &&
|
||||
(f.remoteContent || f.type === 'directory')
|
||||
);
|
||||
|
||||
if (filesToPull.length === 0) {
|
||||
alert('No files to pull');
|
||||
return;
|
||||
}
|
||||
|
||||
showLog(`Pulling ${filesToPull.length} files from server...\n`);
|
||||
|
||||
const result = await window.api.pullFiles({
|
||||
serverId: state.selectedServerId,
|
||||
files: filesToPull
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
appendLog(`\nError: ${result.error}`);
|
||||
} else {
|
||||
if (result.pulled && result.pulled.length > 0) {
|
||||
appendLog(`\nPulled: ${result.pulled.join(', ')}`);
|
||||
}
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
appendLog(`\nErrors:`);
|
||||
for (const err of result.errors) {
|
||||
appendLog(`\n ${err.name}: ${err.error}`);
|
||||
}
|
||||
}
|
||||
appendLog('\n\nDone! Refreshing...');
|
||||
|
||||
// Refresh
|
||||
hideModal(logModal);
|
||||
hideModal(deployConfirmModal);
|
||||
await loadLocalProjects();
|
||||
}
|
||||
};
|
||||
|
||||
function maskEnvContent(content) {
|
||||
if (!content) return null;
|
||||
// Mask values that look like passwords/secrets
|
||||
return content.replace(/^([A-Z_]+PASSWORD|[A-Z_]+SECRET|[A-Z_]+KEY|[A-Z_]+TOKEN)=(.+)$/gm, '$1=****');
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
async function executeDeployment() {
|
||||
const project = state.currentDeployProject;
|
||||
if (!project) return;
|
||||
|
||||
hideModal(deployConfirmModal);
|
||||
showLog(`Deploying ${project.name}...\n`);
|
||||
|
||||
const result = await window.api.deployProject({
|
||||
projectPath: project.path,
|
||||
serverId: state.selectedServerId,
|
||||
remotePath: project.remotePath || `~/containers/${project.name}`
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
appendLog(`\nError: ${result.error}`);
|
||||
} else {
|
||||
// Show uploaded files
|
||||
if (result.uploadedFiles && result.uploadedFiles.length > 0) {
|
||||
appendLog(`\nUploaded files:\n - ${result.uploadedFiles.join('\n - ')}`);
|
||||
}
|
||||
|
||||
appendLog(`\n\n${result.message || 'Deployment complete!'}`);
|
||||
if (result.status) {
|
||||
appendLog(`\n\nContainer status:\n${result.status}`);
|
||||
}
|
||||
if (result.healthy) {
|
||||
appendLog('\n\nHealth check: PASSED');
|
||||
} else {
|
||||
appendLog('\n\nHealth check: PENDING (container may still be starting)');
|
||||
}
|
||||
// Refresh server state
|
||||
await scanServer();
|
||||
}
|
||||
|
||||
state.currentDeployProject = null;
|
||||
state.currentDeployDiff = null;
|
||||
}
|
||||
|
||||
async function openVSCodeDiff() {
|
||||
const project = state.currentDeployProject;
|
||||
const diff = state.currentDeployDiff;
|
||||
if (!project || !diff) return;
|
||||
|
||||
// Open docker-compose.yml diff in VS Code
|
||||
if (diff.dockerCompose.status === 'different') {
|
||||
const result = await window.api.openVSCodeDiff({
|
||||
localPath: `${project.path}/docker-compose.yml`,
|
||||
serverId: state.selectedServerId,
|
||||
remoteFilePath: `${project.remotePath || '~/containers/' + project.name}/docker-compose.yml`
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
alert(`Error opening VS Code: ${result.error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function showDiff(projectName) {
|
||||
const project = state.localProjects.find(p => p.name === projectName);
|
||||
if (!project) return;
|
||||
|
||||
state.currentDiffProject = project;
|
||||
document.getElementById('diff-project-name').textContent = projectName;
|
||||
|
||||
const result = await window.api.compareProject({
|
||||
projectPath: project.path,
|
||||
serverId: state.selectedServerId,
|
||||
remotePath: project.remotePath || `~/containers/${projectName}`
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
alert(`Error comparing: ${result.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const diff = result.diff;
|
||||
|
||||
// docker-compose status
|
||||
const statusEl = document.getElementById('diff-compose-status');
|
||||
if (diff.dockerCompose.status === 'match') {
|
||||
statusEl.innerHTML = '<span class="diff-badge match">Files match</span>';
|
||||
} else if (diff.dockerCompose.status === 'different') {
|
||||
statusEl.innerHTML = '<span class="diff-badge different">Files differ</span>';
|
||||
} else if (diff.dockerCompose.status === 'remote-missing') {
|
||||
statusEl.innerHTML = '<span class="diff-badge missing">Not deployed</span>';
|
||||
} else if (diff.dockerCompose.status === 'local-missing') {
|
||||
statusEl.innerHTML = '<span class="diff-badge missing">No local file</span>';
|
||||
} else {
|
||||
statusEl.innerHTML = '<span class="diff-badge unknown">Unknown</span>';
|
||||
}
|
||||
|
||||
document.getElementById('diff-compose-local').textContent = diff.dockerCompose.localContent || '(not found)';
|
||||
document.getElementById('diff-compose-remote').textContent = diff.dockerCompose.remoteContent || '(not found)';
|
||||
|
||||
// Wire up pull/push buttons
|
||||
document.getElementById('btn-pull-compose').onclick = () => pullCompose(project);
|
||||
document.getElementById('btn-push-compose').onclick = () => pushCompose(project);
|
||||
|
||||
showModal(diffModal);
|
||||
}
|
||||
|
||||
async function pullCompose(project) {
|
||||
const result = await window.api.pullFile({
|
||||
serverId: state.selectedServerId,
|
||||
remotePath: `${project.remotePath || '~/containers/' + project.name}/docker-compose.yml`,
|
||||
localPath: `${project.path}/docker-compose.yml`
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
alert(`Error: ${result.error}`);
|
||||
} else {
|
||||
alert('Pulled docker-compose.yml from server');
|
||||
hideModal(diffModal);
|
||||
loadLocalProjects();
|
||||
}
|
||||
}
|
||||
|
||||
async function pushCompose(project) {
|
||||
// Deploy just the compose file
|
||||
await deployProject(project.name);
|
||||
hideModal(diffModal);
|
||||
}
|
||||
|
||||
// Server management
|
||||
function showServerModal() {
|
||||
renderServerList();
|
||||
clearServerForm();
|
||||
showModal(serverModal);
|
||||
}
|
||||
|
||||
function renderServerList() {
|
||||
const listEl = document.getElementById('server-list');
|
||||
listEl.innerHTML = '';
|
||||
|
||||
for (const server of state.servers) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'server-item';
|
||||
item.innerHTML = `
|
||||
<div class="server-item-info">
|
||||
<span class="server-item-name">${server.name}</span>
|
||||
<span class="server-item-host">${server.host}</span>
|
||||
${server.useSudo ? '<div class="server-item-badges"><span class="server-sudo-badge">sudo</span></div>' : ''}
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-small btn-secondary" onclick="editServer('${server.id}')">Edit</button>
|
||||
<button class="btn btn-small btn-secondary" onclick="deleteServer('${server.id}')">Delete</button>
|
||||
</div>
|
||||
`;
|
||||
listEl.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
function clearServerForm() {
|
||||
document.getElementById('server-id').value = '';
|
||||
document.getElementById('server-name').value = '';
|
||||
document.getElementById('server-host').value = '';
|
||||
document.getElementById('server-username').value = '';
|
||||
document.getElementById('server-use-sudo').checked = false;
|
||||
}
|
||||
|
||||
window.editServer = function(serverId) {
|
||||
const server = state.servers.find(s => s.id === serverId);
|
||||
if (!server) return;
|
||||
|
||||
document.getElementById('server-id').value = server.id;
|
||||
document.getElementById('server-name').value = server.name;
|
||||
document.getElementById('server-host').value = server.host;
|
||||
document.getElementById('server-username').value = server.username || '';
|
||||
document.getElementById('server-use-sudo').checked = server.useSudo || false;
|
||||
};
|
||||
|
||||
window.deleteServer = async function(serverId) {
|
||||
if (!confirm('Delete this server?')) return;
|
||||
|
||||
await window.api.deleteServer(serverId);
|
||||
await loadServers();
|
||||
renderServerList();
|
||||
};
|
||||
|
||||
async function saveServer() {
|
||||
const server = {
|
||||
id: document.getElementById('server-id').value || undefined,
|
||||
name: document.getElementById('server-name').value,
|
||||
host: document.getElementById('server-host').value,
|
||||
username: document.getElementById('server-username').value,
|
||||
useSudo: document.getElementById('server-use-sudo').checked
|
||||
};
|
||||
|
||||
if (!server.name || !server.host) {
|
||||
alert('Name and host are required');
|
||||
return;
|
||||
}
|
||||
|
||||
await window.api.saveServer(server);
|
||||
await loadServers();
|
||||
renderServerList();
|
||||
clearServerForm();
|
||||
}
|
||||
|
||||
// Log modal
|
||||
function showLog(text) {
|
||||
document.getElementById('log-content').textContent = text;
|
||||
showModal(logModal);
|
||||
}
|
||||
|
||||
function appendLog(text) {
|
||||
document.getElementById('log-content').textContent += text;
|
||||
}
|
||||
|
||||
// Modal helpers
|
||||
function showModal(modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideModal(modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Required files checker
|
||||
function getRequiredFiles(project) {
|
||||
return [
|
||||
{
|
||||
name: 'Dockerfile',
|
||||
description: 'Container build instructions',
|
||||
present: project.hasDockerfile,
|
||||
fix: 'Create manually or use: npm run docker-deploy -- init'
|
||||
},
|
||||
{
|
||||
name: 'docker-compose.yml',
|
||||
description: 'Runtime configuration (ports, env, volumes)',
|
||||
present: project.hasDockerCompose,
|
||||
fix: 'Create manually or use: npm run docker-deploy -- init'
|
||||
},
|
||||
{
|
||||
name: 'build-image-tar.ps1',
|
||||
description: 'Script to build Docker image and create tar',
|
||||
present: project.hasBuildScript,
|
||||
fix: 'Use: npm run docker-deploy -- init'
|
||||
},
|
||||
{
|
||||
name: '.dockerignore',
|
||||
description: 'Files to exclude from Docker image',
|
||||
present: project.hasDockerIgnore,
|
||||
fix: 'Create manually or use: npm run docker-deploy -- init'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Show project details modal
|
||||
function showDetails(projectName) {
|
||||
const project = state.localProjects.find(p => p.name === projectName);
|
||||
if (!project) return;
|
||||
|
||||
state.currentDetailsProject = project;
|
||||
document.getElementById('details-project-name').textContent = projectName;
|
||||
|
||||
// Render file checklist
|
||||
const filesListEl = document.getElementById('details-files-list');
|
||||
const requiredFiles = getRequiredFiles(project);
|
||||
|
||||
filesListEl.innerHTML = requiredFiles.map(file => `
|
||||
<div class="file-item">
|
||||
<div class="file-item-icon ${file.present ? 'present' : 'missing'}">
|
||||
${file.present ? '✓' : '✗'}
|
||||
</div>
|
||||
<div class="file-item-info">
|
||||
<div class="file-item-name">${file.name}</div>
|
||||
<div class="file-item-desc">${file.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Render fix instructions
|
||||
const fixEl = document.getElementById('details-fix-instructions');
|
||||
const missingFiles = requiredFiles.filter(f => !f.present);
|
||||
|
||||
if (missingFiles.length === 0) {
|
||||
fixEl.innerHTML = `
|
||||
<div class="fix-step">
|
||||
<div class="fix-step-title">All files present!</div>
|
||||
<p>This project is ready for deployment.</p>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('btn-init-project').style.display = 'none';
|
||||
} else {
|
||||
fixEl.innerHTML = `
|
||||
<div class="fix-step">
|
||||
<div class="fix-step-title">Option 1: Use the CLI tool</div>
|
||||
<p>Run this command to generate missing files:</p>
|
||||
<div class="fix-step-cmd">cd "${project.path}"<br>npm run docker-deploy -- init .</div>
|
||||
<div class="fix-step-note">This runs from the docker-deployment-manager repo</div>
|
||||
</div>
|
||||
<div class="fix-step">
|
||||
<div class="fix-step-title">Option 2: Create manually</div>
|
||||
<p>Missing files:</p>
|
||||
${missingFiles.map(f => `
|
||||
<div class="fix-step-cmd">${f.name} - ${f.description}</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('btn-init-project').style.display = 'block';
|
||||
}
|
||||
|
||||
showModal(detailsModal);
|
||||
}
|
||||
|
||||
// Initialize project via CLI
|
||||
async function initCurrentProject() {
|
||||
const project = state.currentDetailsProject;
|
||||
if (!project) return;
|
||||
|
||||
hideModal(detailsModal);
|
||||
showLog(`Initializing Docker config for ${project.name}...\n\nRunning: npm run docker-deploy -- init "${project.path}" --no-interactive\n\n`);
|
||||
|
||||
const result = await window.api.initProject(project.path);
|
||||
|
||||
if (result.error) {
|
||||
appendLog(`\nError: ${result.error}`);
|
||||
} else {
|
||||
appendLog(result.output);
|
||||
appendLog('\n\nInit complete! Refreshing project list...');
|
||||
await loadLocalProjects();
|
||||
}
|
||||
}
|
||||
|
||||
// Container logs viewer
|
||||
async function viewLogs(projectName) {
|
||||
const project = state.localProjects.find(p => p.name === projectName);
|
||||
if (!project || !state.selectedServerId) return;
|
||||
|
||||
state.currentLogsProject = project;
|
||||
document.getElementById('logs-container-name').textContent = projectName;
|
||||
document.getElementById('logs-content').textContent = 'Loading logs...';
|
||||
|
||||
showModal(logsModal);
|
||||
await refreshLogs();
|
||||
}
|
||||
|
||||
async function refreshLogs() {
|
||||
const project = state.currentLogsProject;
|
||||
if (!project) return;
|
||||
|
||||
const result = await window.api.getContainerLogs({
|
||||
serverId: state.selectedServerId,
|
||||
remotePath: project.remotePath || `~/containers/${project.name}`,
|
||||
lines: 100
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
document.getElementById('logs-content').textContent = `Error: ${result.error}`;
|
||||
} else {
|
||||
document.getElementById('logs-content').textContent = result.logs || '(no logs)';
|
||||
}
|
||||
}
|
||||
|
||||
// Global function bindings for inline onclick
|
||||
window.buildTar = buildTar;
|
||||
window.deployProject = deployProject;
|
||||
window.showDiff = showDiff;
|
||||
window.showDetails = showDetails;
|
||||
window.viewLogs = viewLogs;
|
||||
|
||||
// Start app
|
||||
init();
|
||||
193
app/renderer-legacy/index.html
Normal file
193
app/renderer-legacy/index.html
Normal file
@@ -0,0 +1,193 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'">
|
||||
<title>Docker Deployment Manager</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<header class="header">
|
||||
<h1>Docker Deployment Manager</h1>
|
||||
<div class="header-actions">
|
||||
<button id="btn-refresh" class="btn btn-secondary">Refresh</button>
|
||||
<button id="btn-settings" class="btn btn-secondary">Servers</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="server-bar">
|
||||
<label>Server:</label>
|
||||
<select id="server-select">
|
||||
<option value="">-- Select Server --</option>
|
||||
</select>
|
||||
<button id="btn-scan-server" class="btn btn-primary" disabled>Scan Server</button>
|
||||
<span id="server-status" class="status-badge"></span>
|
||||
</div>
|
||||
|
||||
<main class="main">
|
||||
<div class="projects-table-container">
|
||||
<table class="projects-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Project</th>
|
||||
<th>Local</th>
|
||||
<th>Deployed</th>
|
||||
<th>Running</th>
|
||||
<th>Diff</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="projects-body">
|
||||
<tr>
|
||||
<td colspan="6" class="loading">Loading projects...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Server Modal -->
|
||||
<div id="server-modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Manage Servers</h2>
|
||||
<button class="btn-close" id="close-server-modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="server-list" class="server-list"></div>
|
||||
<div class="server-form">
|
||||
<h3>Add/Edit Server</h3>
|
||||
<input type="hidden" id="server-id">
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" id="server-name" placeholder="e.g., Production Server">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Host:</label>
|
||||
<input type="text" id="server-host" placeholder="e.g., 192.168.69.4">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Username:</label>
|
||||
<input type="text" id="server-username" placeholder="From .env (SSH_USERNAME)">
|
||||
</div>
|
||||
<div class="form-group checkbox-group">
|
||||
<label>
|
||||
<input type="checkbox" id="server-use-sudo">
|
||||
Require sudo for Docker commands
|
||||
</label>
|
||||
</div>
|
||||
<p class="hint">Username and password are read from .env file (SSH_USERNAME, SSH_PASSWORD)</p>
|
||||
<button id="btn-save-server" class="btn btn-primary">Save Server</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Diff Modal -->
|
||||
<div id="diff-modal" class="modal hidden">
|
||||
<div class="modal-content modal-wide">
|
||||
<div class="modal-header">
|
||||
<h2>File Comparison: <span id="diff-project-name"></span></h2>
|
||||
<button class="btn-close" id="close-diff-modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="diff-section">
|
||||
<h3>docker-compose.yml</h3>
|
||||
<div class="diff-status" id="diff-compose-status"></div>
|
||||
<div class="diff-container">
|
||||
<div class="diff-panel">
|
||||
<h4>Local</h4>
|
||||
<pre id="diff-compose-local"></pre>
|
||||
</div>
|
||||
<div class="diff-panel">
|
||||
<h4>Remote</h4>
|
||||
<pre id="diff-compose-remote"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="diff-actions">
|
||||
<button id="btn-pull-compose" class="btn btn-secondary">Pull from Server</button>
|
||||
<button id="btn-push-compose" class="btn btn-primary">Push to Server</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log Modal -->
|
||||
<div id="log-modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Action Log</h2>
|
||||
<button class="btn-close" id="close-log-modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="log-content" class="log-output"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details Modal (Missing Files) -->
|
||||
<div id="details-modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Project Status: <span id="details-project-name"></span></h2>
|
||||
<button class="btn-close" id="close-details-modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="details-section">
|
||||
<h3>Required Files</h3>
|
||||
<div id="details-files-list" class="file-checklist"></div>
|
||||
</div>
|
||||
<div class="details-section">
|
||||
<h3>How to Fix</h3>
|
||||
<div id="details-fix-instructions" class="fix-instructions"></div>
|
||||
</div>
|
||||
<div class="details-actions">
|
||||
<button id="btn-init-project" class="btn btn-primary">Run CLI Init</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deploy Confirmation Modal -->
|
||||
<div id="deploy-confirm-modal" class="modal hidden">
|
||||
<div class="modal-content modal-wide">
|
||||
<div class="modal-header">
|
||||
<h2>Confirm Deployment: <span id="deploy-confirm-project"></span></h2>
|
||||
<button class="btn-close" id="close-deploy-confirm">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="deploy-diff-summary" class="deploy-diff-summary"></div>
|
||||
<div id="deploy-diff-details" class="deploy-diff-details"></div>
|
||||
<div class="deploy-confirm-actions">
|
||||
<button id="btn-deploy-continue" class="btn btn-primary">Continue (Overwrite Remote)</button>
|
||||
<button id="btn-deploy-abort" class="btn btn-secondary">Abort</button>
|
||||
<button id="btn-deploy-vscode" class="btn btn-secondary">View in VS Code</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Container Logs Modal -->
|
||||
<div id="logs-modal" class="modal hidden">
|
||||
<div class="modal-content modal-wide">
|
||||
<div class="modal-header">
|
||||
<h2>Container Logs: <span id="logs-container-name"></span></h2>
|
||||
<button class="btn-close" id="close-logs-modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="logs-controls">
|
||||
<button id="btn-refresh-logs" class="btn btn-secondary btn-small">Refresh</button>
|
||||
<span class="logs-hint">Last 100 lines</span>
|
||||
</div>
|
||||
<pre id="logs-content" class="log-output"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
652
app/renderer-legacy/styles.css
Normal file
652
app/renderer-legacy/styles.css
Normal file
@@ -0,0 +1,652 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: #1a1a2e;
|
||||
color: #eee;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
background: #16213e;
|
||||
border-bottom: 1px solid #0f3460;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #e94560;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.server-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 24px;
|
||||
background: #16213e;
|
||||
border-bottom: 1px solid #0f3460;
|
||||
}
|
||||
|
||||
.server-bar label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.server-bar select {
|
||||
padding: 8px 12px;
|
||||
background: #0f3460;
|
||||
border: 1px solid #1a1a2e;
|
||||
color: #eee;
|
||||
border-radius: 4px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge.connected {
|
||||
background: #0d7377;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.status-badge.error {
|
||||
background: #e94560;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #e94560;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #ff6b6b;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #0f3460;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #16213e;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
padding: 4px 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Table */
|
||||
.projects-table-container {
|
||||
background: #16213e;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.projects-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.projects-table th,
|
||||
.projects-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #0f3460;
|
||||
}
|
||||
|
||||
.projects-table th {
|
||||
background: #0f3460;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.projects-table tr:hover {
|
||||
background: rgba(233, 69, 96, 0.1);
|
||||
}
|
||||
|
||||
.projects-table .loading {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
/* Status indicators */
|
||||
.indicator {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.indicator.green { background: #0d7377; }
|
||||
.indicator.yellow { background: #f39c12; }
|
||||
.indicator.red { background: #e94560; }
|
||||
.indicator.gray { background: #555; }
|
||||
|
||||
.status-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.diff-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.diff-badge.match { background: #0d7377; color: #fff; }
|
||||
.diff-badge.different { background: #f39c12; color: #000; }
|
||||
.diff-badge.missing { background: #e94560; color: #fff; }
|
||||
.diff-badge.warning { background: #f39c12; color: #000; }
|
||||
.diff-badge.unknown { background: #555; color: #fff; }
|
||||
|
||||
.actions-cell {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #16213e;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 90vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal-content.modal-wide {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #0f3460;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #888;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-close:hover {
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Server form */
|
||||
.server-list {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.server-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: #0f3460;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.server-item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.server-item-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.server-item-host {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: #0f3460;
|
||||
border: 1px solid #1a1a2e;
|
||||
border-radius: 4px;
|
||||
color: #eee;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Diff view */
|
||||
.diff-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.diff-section h3 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.diff-status {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.diff-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.diff-panel {
|
||||
background: #0f3460;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.diff-panel h4 {
|
||||
padding: 8px 12px;
|
||||
background: #1a1a2e;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.diff-panel pre {
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
overflow: auto;
|
||||
max-height: 300px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.diff-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Log output */
|
||||
.log-output {
|
||||
background: #0f3460;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Project name styling */
|
||||
.project-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.project-path {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Details modal - file checklist */
|
||||
.details-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.details-section h3 {
|
||||
margin-bottom: 12px;
|
||||
color: #e94560;
|
||||
}
|
||||
|
||||
.file-checklist {
|
||||
background: #0f3460;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #1a1a2e;
|
||||
}
|
||||
|
||||
.file-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.file-item-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-item-icon.present {
|
||||
background: #0d7377;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.file-item-icon.missing {
|
||||
background: #e94560;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.file-item-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.file-item-name {
|
||||
font-weight: 500;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.file-item-desc {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* Fix instructions */
|
||||
.fix-instructions {
|
||||
background: #0f3460;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.fix-step {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #1a1a2e;
|
||||
}
|
||||
|
||||
.fix-step:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.fix-step-title {
|
||||
font-weight: 500;
|
||||
color: #f39c12;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.fix-step-cmd {
|
||||
background: #1a1a2e;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.fix-step-note {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.details-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* Issues badge */
|
||||
.issues-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.issues-badge.ok {
|
||||
background: #0d7377;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.issues-badge.warning {
|
||||
background: #f39c12;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.issues-badge.error {
|
||||
background: #e94560;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.issues-badge:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Checkbox group */
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Deploy confirmation modal */
|
||||
.deploy-diff-summary {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #0f3460;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.deploy-diff-summary.has-diff {
|
||||
border-left: 4px solid #f39c12;
|
||||
}
|
||||
|
||||
.deploy-diff-summary.no-diff {
|
||||
border-left: 4px solid #0d7377;
|
||||
}
|
||||
|
||||
.deploy-diff-details {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.deploy-file-diff {
|
||||
margin-bottom: 16px;
|
||||
background: #0f3460;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.deploy-file-diff h4 {
|
||||
padding: 8px 12px;
|
||||
background: #1a1a2e;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.deploy-file-diff .diff-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1px;
|
||||
background: #1a1a2e;
|
||||
}
|
||||
|
||||
.deploy-file-diff .diff-side {
|
||||
background: #0f3460;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.deploy-file-diff .diff-side-header {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.deploy-file-diff pre {
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.deploy-confirm-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #0f3460;
|
||||
}
|
||||
|
||||
/* Logs modal */
|
||||
.logs-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.logs-hint {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
#logs-content {
|
||||
max-height: 500px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
/* Server item with sudo badge */
|
||||
.server-item-badges {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.server-sudo-badge {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
background: #f39c12;
|
||||
color: #000;
|
||||
border-radius: 3px;
|
||||
}
|
||||
Reference in New Issue
Block a user