923 lines
29 KiB
JavaScript
923 lines
29 KiB
JavaScript
// 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();
|