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();
|
||||
Reference in New Issue
Block a user