|
|
|
@@ -453,7 +453,10 @@ function renderGitTools(repository) {
|
|
|
|
|
|
|
|
|
|
function renderRepositorySettings(repository) {
|
|
|
|
|
const automaticTarget = displayCloneTarget(repository);
|
|
|
|
|
return `<div class="tab-page"><section class="settings-group"><h2>Repository identity</h2><div class="form-grid"><div class="field full"><label>Gitea repository</label><input class="input" value="${attr(repository.fullName)}" readonly/></div><div class="field full"><label>Local working tree</label><input class="input mono" value="${attr(repository.localPath || automaticTarget || 'Not linked')}" readonly/></div></div><div class="card-actions"><button class="button" data-action="${repository.localPath ? 'open-path' : 'link-repo'}">${icon('folder')}${repository.localPath ? 'Open project folder' : 'Link local folder'}</button>${repository.localPath ? `<button class="button danger" data-action="unlink-repo">${icon('link')}Remove link</button>` : `<button class="button primary" data-action="clone-repo">${icon('cloud')}${escapeHtml(clonePrimaryLabel(repository))}</button><button class="button ghost" data-action="clone-repo-custom">Choose another location</button>`}</div></section><section class="settings-group"><h2>Repository behavior</h2><div class="notice">${icon('shield')}ForgeFlow automatically creates a repository-named subfolder and never overwrites a non-empty conflicting folder. Existing matching clones are linked instead.</div></section></div>`;
|
|
|
|
|
const currentOrigin = repository.localStatus?.remoteUrl || 'Unavailable';
|
|
|
|
|
const desiredOrigin = repository.sshUrl || repository.preferredCloneUrl || '';
|
|
|
|
|
const originNeedsRepair = Boolean(repository.localPath && desiredOrigin && currentOrigin !== desiredOrigin);
|
|
|
|
|
return `<div class="tab-page"><section class="settings-group"><h2>Repository identity</h2><div class="form-grid"><div class="field full"><label>Gitea repository</label><input class="input" value="${attr(repository.fullName)}" readonly/></div><div class="field full"><label>Local working tree</label><input class="input mono" value="${attr(repository.localPath || automaticTarget || 'Not linked')}" readonly/></div><div class="field full"><label>Current origin</label><input class="input mono" value="${attr(currentOrigin)}" readonly/></div>${desiredOrigin ? `<div class="field full"><label>Current Gitea SSH origin</label><input class="input mono" value="${attr(desiredOrigin)}" readonly/></div>` : ''}</div><div class="card-actions"><button class="button" data-action="${repository.localPath ? 'open-path' : 'link-repo'}">${icon('folder')}${repository.localPath ? 'Open project folder' : 'Link local folder'}</button>${originNeedsRepair ? `<button class="button primary" data-action="repair-origin">${icon('link')}Use current Gitea origin</button>` : ''}${repository.localPath ? `<button class="button" data-action="repair-index-lock">${icon('key')}Repair stale Git lock</button><button class="button danger" data-action="unlink-repo">${icon('link')}Remove link</button>` : `<button class="button primary" data-action="clone-repo">${icon('cloud')}${escapeHtml(clonePrimaryLabel(repository))}</button><button class="button ghost" data-action="clone-repo-custom">Choose another location</button>`}</div></section><section class="settings-group"><h2>Repository behavior</h2><div class="notice">${icon('shield')}Origin repair changes only the Git remote URL. Lock repair refuses recent locks and never changes files or commits.</div></section></div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderRepositoryWorkspace(repository) {
|
|
|
|
@@ -508,6 +511,7 @@ function renderSettings() {
|
|
|
|
|
<section class="settings-group"><h2>Gitea connection</h2><div class="form-grid"><div class="field full"><label for="settings-gitea-url">Instance URL</label><input id="settings-gitea-url" class="input" value="${attr(state.gitea.baseUrl)}" placeholder="https://gitea.example.com" /></div><div class="field full"><label for="settings-gitea-token">New access token</label><input id="settings-gitea-token" class="input" type="password" placeholder="Leave empty to keep the existing token" /></div></div><div class="connection-card" style="margin-top:10px"><div><strong>${state.gitea.hasToken ? `Connected as ${escapeHtml(state.gitea.user?.login || 'user')}` : 'Not connected'}</strong><div class="queue-sub">${escapeHtml(state.gitea.baseUrl || 'No Gitea instance configured')}</div></div><button class="button primary" data-action="save-gitea-settings">Validate & save</button></div></section>
|
|
|
|
|
<section class="settings-group"><div class="section-heading"><div><h2>ForgeFlow updates</h2><span class="meta">Secure source update from ${escapeHtml(state.updates?.owner || 'Jens')}/${escapeHtml(state.updates?.repo || 'ForgeFlow')}</span></div><button class="button" data-action="check-updates" ${ui.updateChecking ? 'disabled' : ''}>${icon('update')}${ui.updateChecking ? 'Checking…' : 'Check now'}</button></div><div class="form-grid"><div class="field"><label>Repository owner</label><input id="update-owner" class="input" value="${attr(state.updates?.owner || 'Jens')}"/></div><div class="field"><label>Repository name</label><input id="update-repo" class="input" value="${attr(state.updates?.repo || 'ForgeFlow')}"/></div><div class="field"><label>Release branch</label><input id="update-branch" class="input" value="${attr(state.updates?.branch || 'main')}"/></div><div class="field"><label>Automatic startup check</label><select id="update-auto-check" class="select"><option value="true" ${state.updates?.autoCheck !== false ? 'selected' : ''}>Enabled</option><option value="false" ${state.updates?.autoCheck === false ? 'selected' : ''}>Disabled</option></select></div></div><div class="update-card ${update?.available ? 'available' : ''}"><div>${icon(update?.available ? 'download' : 'check')}<span><strong>${update ? (update.available ? `ForgeFlow ${escapeHtml(update.remoteVersion)} is available` : `ForgeFlow ${escapeHtml(update.currentVersion)} is up to date`) : `Current version ${escapeHtml(ui.boot.appVersion)}`}</strong><small>${update ? `Branch ${escapeHtml(update.branch)} · commit ${escapeHtml(update.shortSha)} · checked ${formatDate(update.checkedAt)}` : 'No update check in this session.'}</small></span></div><div class="stack horizontal compact">${update?.available && !update.downloaded ? `<button class="button primary" data-action="download-update">${icon('download')}Download update</button>` : ''}${update?.downloaded ? `<button class="button success" data-action="apply-update">${icon('update')}Apply & restart</button>` : ''}<button class="button" data-action="save-update-settings">Save update settings</button></div></div><div class="notice" style="margin-top:10px">${icon('shield')}The updater downloads an authenticated ZIP for the exact remote commit, verifies its SHA-256 checksum, runs the complete quality gate and restores the previous source version if validation fails.</div></section>
|
|
|
|
|
<section class="settings-group"><div class="section-heading"><div><h2>SSH / Unraid servers</h2><span class="meta">Credentials are entered locally and encrypted with the Windows credential protection used by Electron.</span></div><button class="button primary" data-action="open-add-server">${icon('plus')}Add server</button></div>${servers.length ? `<div class="server-list">${servers.map((server) => `<article class="server-card"><div class="server-card-main">${icon('server')}<div><strong>${escapeHtml(server.name)}</strong><span>${escapeHtml(server.username)}@${escapeHtml(server.host)}:${escapeHtml(server.port)} · ${escapeHtml(server.basePath)}</span><small>${server.hostFingerprint ? `Trusted ${escapeHtml(server.hostFingerprint)}` : 'Host identity not trusted yet'}</small></div></div><div class="stack horizontal compact"><button class="button" data-action="test-server" data-server-id="${attr(server.id)}">Test & trust</button><button class="button" data-action="edit-server" data-server-id="${attr(server.id)}">Edit</button><button class="icon-button danger" data-action="delete-server" data-server-id="${attr(server.id)}" title="Delete server">${icon('trash')}</button></div></article>`).join('')}</div>` : '<div class="empty-state compact"><p>No SSH server configured. Add your Unraid server before creating an SSH deployment profile.</p></div>'}</section>
|
|
|
|
|
<section class="settings-group"><div class="section-heading"><div><h2>Git remote maintenance</h2><span class="meta">Standardize linked repositories to the current Gitea SSH URLs.</span></div><button class="button" data-action="normalize-origins">${icon('link')}Normalize all origins</button></div><p>This replaces legacy aliases and renamed owners only after an explicit click. Local commits and files are not changed.</p></section>
|
|
|
|
|
<section class="settings-group"><h2>Project roots</h2><p>The first folder is the default clone destination. ForgeFlow automatically creates one subfolder per repository.</p><div class="stack">${state.workspaceRoots.map((root, index) => `<div class="root-row">${index === 0 ? '<span class="status-pill success">Default</span>' : ''}<input class="input" data-root-index="${index}" value="${attr(root)}"/><button class="icon-button" data-action="remove-root" data-index="${index}" title="Remove">${icon('trash')}</button></div>`).join('')}<button class="button" data-action="add-root">${icon('plus')}Add project root</button><button class="button primary" data-action="save-roots">Save folders & rescan</button></div></section>
|
|
|
|
|
<section class="settings-group"><h2>Background awareness</h2><div class="form-grid"><div class="field"><label>Automatic repository refresh</label><select id="pref-auto-refresh" class="select"><option value="true" ${prefs.autoRefresh !== false ? 'selected' : ''}>Enabled</option><option value="false" ${prefs.autoRefresh === false ? 'selected' : ''}>Disabled</option></select></div><div class="field"><label>Local poll interval</label><input id="pref-repo-poll" class="input" type="number" min="2" max="60" value="${attr(prefs.repositoryPollSeconds || 4)}"/></div><div class="field"><label>Actions poll interval</label><input id="pref-operation-poll" class="input" type="number" min="3" max="120" value="${attr(prefs.operationPollSeconds || 5)}"/></div><div class="field"><label>Preferred clone protocol</label><select id="pref-clone-protocol" class="select"><option value="https" ${prefs.preferredCloneProtocol !== 'ssh' ? 'selected' : ''}>HTTPS</option><option value="ssh" ${prefs.preferredCloneProtocol === 'ssh' ? 'selected' : ''}>SSH</option></select></div></div><button class="button primary" style="margin-top:12px" data-action="save-preferences">Save awareness settings</button></section>
|
|
|
|
|
<section class="settings-group"><h2>Appearance</h2><select id="appearance-select" class="select"><option value="dark" ${state.appearance === 'dark' ? 'selected' : ''}>Dark</option><option value="light" ${state.appearance === 'light' ? 'selected' : ''}>Light</option><option value="system" ${state.appearance === 'system' ? 'selected' : ''}>Follow system</option></select></section>
|
|
|
|
@@ -584,7 +588,7 @@ function renderModal() {
|
|
|
|
|
const provider = ui.modal.provider || existing.provider || (servers.length ? 'ssh-unraid' : 'gitea-actions');
|
|
|
|
|
const ssh = provider === 'ssh-unraid';
|
|
|
|
|
const remoteFolder = existing.remoteFolder || safeCloneFolderName(repository);
|
|
|
|
|
return `<div class="modal-backdrop"><section class="modal wide-modal"><header class="modal-header"><h2>${existing.id ? 'Edit' : 'Add'} deployment environment</h2><button class="icon-button" data-action="close-modal">${icon('close')}</button></header><div class="modal-body"><div class="form-grid"><div class="field full"><label>Deployment provider</label><select id="profile-provider" class="select"><option value="ssh-unraid" ${ssh ? 'selected' : ''}>SSH / Unraid · direct controlled deployment</option><option value="gitea-actions" ${!ssh ? 'selected' : ''}>Gitea Actions · runner workflow</option></select></div><div class="field"><label>Profile name</label><input id="profile-name" class="input" value="${attr(existing.name || 'Production')}" /></div><div class="field"><label>Environment</label><input id="profile-environment" class="input" value="${attr(existing.environment || 'production')}" /></div><div class="field"><label>Allowed branch</label><input id="profile-branch" class="input" value="${attr(existing.branch || repository?.defaultBranch || 'main')}" /></div>${ssh ? `
|
|
|
|
|
return `<div class="modal-backdrop" role="presentation"><section class="modal wide-modal" role="dialog" aria-modal="true"><header class="modal-header"><h2>${existing.id ? 'Edit' : 'Add'} deployment environment</h2><button class="icon-button" data-action="close-modal">${icon('close')}</button></header><div class="modal-body"><div class="form-grid"><div class="field full"><label>Deployment provider</label><select id="profile-provider" class="select"><option value="ssh-unraid" ${ssh ? 'selected' : ''}>SSH / Unraid · direct controlled deployment</option><option value="gitea-actions" ${!ssh ? 'selected' : ''}>Gitea Actions · runner workflow</option></select></div><div class="field"><label>Profile name</label><input id="profile-name" class="input" value="${attr(existing.name || 'Production')}" /></div><div class="field"><label>Environment</label><input id="profile-environment" class="input" value="${attr(existing.environment || 'production')}" /></div><div class="field"><label>Allowed branch</label><input id="profile-branch" class="input" value="${attr(existing.branch || repository?.defaultBranch || 'main')}" /></div>${ssh ? `
|
|
|
|
|
<div class="field"><label>Unraid server</label><select id="profile-server" class="select">${servers.length ? servers.map((server) => `<option value="${attr(server.id)}" ${server.id === existing.serverId ? 'selected' : ''}>${escapeHtml(server.name)} · ${escapeHtml(server.host)}</option>`).join('') : '<option value="">Configure a server first</option>'}</select></div>
|
|
|
|
|
<div class="field"><label>Server folder name</label><input id="profile-remote-folder" class="input" value="${attr(remoteFolder)}"/></div>
|
|
|
|
|
<div class="field"><label>Git clone URL used by Unraid</label><input id="profile-clone-url" class="input" value="${attr(existing.cloneUrl || repository?.sshUrl || '')}" placeholder="ssh://git@gitea:222/Jens/project.git"/></div>
|
|
|
|
@@ -607,21 +611,21 @@ function renderModal() {
|
|
|
|
|
if (ui.modal.type === 'deployment-preflight') {
|
|
|
|
|
const profile = repository?.deploymentProfiles?.find((item) => item.id === ui.modal.profileId) || selectedProfile(repository);
|
|
|
|
|
const report = ui.deploymentPreflight;
|
|
|
|
|
return `<div class="modal-backdrop"><section class="modal wide-modal"><header class="modal-header"><h2>Deployment preflight</h2><button class="icon-button" data-action="close-modal">${icon('close')}</button></header><div class="modal-body"><div class="confirm-hero ${report?.summary?.ready ? '' : 'danger'}">${icon(report?.summary?.ready ? 'shield' : 'error')}<div><strong>${report?.summary?.ready ? 'Environment is ready to test' : 'Deployment is blocked'}</strong><span>${escapeHtml(repository?.fullName || '')} → ${escapeHtml(profile?.environment || '')}</span></div></div><div class="preflight-summary"><span class="status-pill ${report?.summary?.ready ? 'success' : 'danger'}">${report?.summary?.ready ? 'Ready' : `${report?.summary?.blocking?.length || 0} blocking`}</span><span>${report?.summary?.counts?.pass || 0} passed · ${report?.summary?.counts?.warning || 0} warnings · ${report?.summary?.counts?.fail || 0} failed</span></div>${renderPreflightChecks(report)}</div><footer class="modal-footer"><button class="button" data-action="close-modal">Close</button>${report?.summary?.ready ? `<button class="button success" data-action="continue-after-preflight" data-profile-id="${attr(profile?.id || '')}">${icon('rocket')}Continue</button>` : `<button class="button" data-action="edit-deployment-profile" data-profile-id="${attr(profile?.id || '')}">Edit environment</button>`}</footer></section></div>`;
|
|
|
|
|
return `<div class="modal-backdrop" role="presentation"><section class="modal wide-modal" role="dialog" aria-modal="true"><header class="modal-header"><h2>Deployment preflight</h2><button class="icon-button" data-action="close-modal">${icon('close')}</button></header><div class="modal-body"><div class="confirm-hero ${report?.summary?.ready ? '' : 'danger'}">${icon(report?.summary?.ready ? 'shield' : 'error')}<div><strong>${report?.summary?.ready ? 'Environment is ready to test' : 'Deployment is blocked'}</strong><span>${escapeHtml(repository?.fullName || '')} → ${escapeHtml(profile?.environment || '')}</span></div></div><div class="preflight-summary"><span class="status-pill ${report?.summary?.ready ? 'success' : 'danger'}">${report?.summary?.ready ? 'Ready' : `${report?.summary?.blocking?.length || 0} blocking`}</span><span>${report?.summary?.counts?.pass || 0} passed · ${report?.summary?.counts?.warning || 0} warnings · ${report?.summary?.counts?.fail || 0} failed</span></div>${renderPreflightChecks(report)}</div><footer class="modal-footer"><button class="button" data-action="close-modal">Close</button>${report?.summary?.ready ? `<button class="button success" data-action="continue-after-preflight" data-profile-id="${attr(profile?.id || '')}">${icon('rocket')}Continue</button>` : `<button class="button" data-action="edit-deployment-profile" data-profile-id="${attr(profile?.id || '')}">Edit environment</button>`}</footer></section></div>`;
|
|
|
|
|
}
|
|
|
|
|
if (ui.modal.type === 'deploy-confirm') {
|
|
|
|
|
const profile = repository?.deploymentProfiles?.find((item) => item.id === ui.modal.profileId) || selectedProfile(repository);
|
|
|
|
|
return `<div class="modal-backdrop"><section class="modal"><header class="modal-header"><h2>Confirm production action</h2><button class="icon-button" data-action="close-modal">${icon('close')}</button></header><div class="modal-body"><div class="confirm-hero">${icon('rocket')}<div><strong>Deploy ${escapeHtml(repository.localStatus.shortHead)} → ${escapeHtml(profile.environment)}</strong><span>${escapeHtml(repository.fullName)}</span></div></div><div class="confirm-grid"><span>Exact commit</span><strong class="mono">${escapeHtml(repository.localStatus.head)}</strong><span>Branch</span><strong>${escapeHtml(profile.branch)}</strong><span>Provider</span><strong>${profile.provider === 'ssh-unraid' ? `SSH → ${escapeHtml(profile.remoteFolder)}` : escapeHtml(profile.workflowFile)}</strong><span>Healthcheck</span><strong>${escapeHtml(profile.healthcheckUrl || 'Not configured')}</strong></div>${ui.deploymentPreflight ? `<div class="notice success" style="margin-top:12px">${icon('shield')}Preflight passed with ${ui.deploymentPreflight.summary.counts.warning} warning(s). Backend safety checks run again at dispatch time.</div>` : ''}</div><footer class="modal-footer"><button class="button" data-action="close-modal">Cancel</button><button class="button success" data-action="confirm-deploy" data-profile-id="${attr(profile.id)}">Deploy exact commit</button></footer></section></div>`;
|
|
|
|
|
return `<div class="modal-backdrop" role="presentation"><section class="modal" role="dialog" aria-modal="true"><header class="modal-header"><h2>Confirm production action</h2><button class="icon-button" data-action="close-modal">${icon('close')}</button></header><div class="modal-body"><div class="confirm-hero">${icon('rocket')}<div><strong>Deploy ${escapeHtml(repository.localStatus.shortHead)} → ${escapeHtml(profile.environment)}</strong><span>${escapeHtml(repository.fullName)}</span></div></div><div class="confirm-grid"><span>Exact commit</span><strong class="mono">${escapeHtml(repository.localStatus.head)}</strong><span>Branch</span><strong>${escapeHtml(profile.branch)}</strong><span>Provider</span><strong>${profile.provider === 'ssh-unraid' ? `SSH → ${escapeHtml(profile.remoteFolder)}` : escapeHtml(profile.workflowFile)}</strong><span>Healthcheck</span><strong>${escapeHtml(profile.healthcheckUrl || 'Not configured')}</strong></div>${ui.deploymentPreflight ? `<div class="notice success" style="margin-top:12px">${icon('shield')}Preflight passed with ${ui.deploymentPreflight.summary.counts.warning} warning(s). Backend safety checks run again at dispatch time.</div>` : ''}</div><footer class="modal-footer"><button class="button" data-action="close-modal">Cancel</button><button class="button success" data-action="confirm-deploy" data-profile-id="${attr(profile.id)}">Deploy exact commit</button></footer></section></div>`;
|
|
|
|
|
}
|
|
|
|
|
if (ui.modal.type === 'rollback-confirm') {
|
|
|
|
|
const profile = repository?.deploymentProfiles?.find((item) => item.id === ui.modal.profileId);
|
|
|
|
|
const target = profile?.state?.previousSha;
|
|
|
|
|
return `<div class="modal-backdrop"><section class="modal"><header class="modal-header"><h2>Confirm rollback</h2><button class="icon-button" data-action="close-modal">${icon('close')}</button></header><div class="modal-body"><div class="confirm-hero danger">${icon('undo')}<div><strong>Rollback ${escapeHtml(profile?.environment || '')} to ${shortSha(target)}</strong><span>The target must still exist on origin/${escapeHtml(profile?.branch || '')}.</span></div></div><div class="confirm-grid"><span>Target commit</span><strong class="mono">${escapeHtml(target || 'Unavailable')}</strong><span>Provider</span><strong>${profile?.provider === 'ssh-unraid' ? 'SSH exact-SHA reset' : escapeHtml(profile?.rollbackWorkflowFile || 'Not configured')}</strong><span>Current live</span><strong class="mono">${escapeHtml(profile?.state?.liveSha || 'Unknown')}</strong></div></div><footer class="modal-footer"><button class="button" data-action="close-modal">Cancel</button><button class="button danger" data-action="confirm-rollback" data-profile-id="${attr(profile?.id || '')}" ${target ? '' : 'disabled'}>Rollback exact commit</button></footer></section></div>`;
|
|
|
|
|
return `<div class="modal-backdrop" role="presentation"><section class="modal" role="dialog" aria-modal="true"><header class="modal-header"><h2>Confirm rollback</h2><button class="icon-button" data-action="close-modal">${icon('close')}</button></header><div class="modal-body"><div class="confirm-hero danger">${icon('undo')}<div><strong>Rollback ${escapeHtml(profile?.environment || '')} to ${shortSha(target)}</strong><span>The target must still exist on origin/${escapeHtml(profile?.branch || '')}.</span></div></div><div class="confirm-grid"><span>Target commit</span><strong class="mono">${escapeHtml(target || 'Unavailable')}</strong><span>Provider</span><strong>${profile?.provider === 'ssh-unraid' ? 'SSH exact-SHA reset' : escapeHtml(profile?.rollbackWorkflowFile || 'Not configured')}</strong><span>Current live</span><strong class="mono">${escapeHtml(profile?.state?.liveSha || 'Unknown')}</strong></div></div><footer class="modal-footer"><button class="button" data-action="close-modal">Cancel</button><button class="button danger" data-action="confirm-rollback" data-profile-id="${attr(profile?.id || '')}" ${target ? '' : 'disabled'}>Rollback exact commit</button></footer></section></div>`;
|
|
|
|
|
}
|
|
|
|
|
if (ui.modal.type === 'server-config') {
|
|
|
|
|
const server = (ui.boot.state.servers || []).find((item) => item.id === ui.modal.serverId) || {};
|
|
|
|
|
const authType = ui.modal.authType || server.authType || 'privateKey';
|
|
|
|
|
return `<div class="modal-backdrop"><section class="modal wide-modal"><header class="modal-header"><h2>${server.id ? 'Edit' : 'Add'} SSH / Unraid server</h2><button class="icon-button" data-action="close-modal">${icon('close')}</button></header><div class="modal-body"><div class="form-grid"><div class="field"><label>Name</label><input id="server-name" class="input" value="${attr(server.name || 'Unraid')}"/></div><div class="field"><label>Host or IP</label><input id="server-host" class="input" value="${attr(server.host || '')}" placeholder="192.168.1.10"/></div><div class="field"><label>SSH port</label><input id="server-port" class="input" type="number" min="1" max="65535" value="${attr(server.port || 22)}"/></div><div class="field"><label>Username</label><input id="server-username" class="input" value="${attr(server.username || 'root')}"/></div><div class="field"><label>Authentication</label><select id="server-auth-type" class="select"><option value="privateKey" ${authType === 'privateKey' ? 'selected' : ''}>Private key · recommended</option><option value="password" ${authType === 'password' ? 'selected' : ''}>Password</option></select></div><div class="field"><label>Appdata base path</label><input id="server-base-path" class="input" value="${attr(server.basePath || '/mnt/user/appdata')}"/></div>${authType === 'privateKey' ? `<div class="field full"><label>Private key file</label><div class="input-action"><input id="server-private-key" class="input" value="${attr(server.privateKeyPath || '')}" placeholder="C:\\Users\\Jens\\.ssh\\id_ed25519"/><button class="button" data-action="select-private-key">Browse</button></div></div><div class="field full"><label>Private key passphrase</label><input id="server-passphrase" class="input" type="password" placeholder="${server.hasPassphrase ? 'Leave empty to keep stored passphrase' : 'Only when the key is encrypted'}"/></div>` : `<div class="field full"><label>SSH password</label><input id="server-password" class="input" type="password" placeholder="${server.hasPassword ? 'Leave empty to keep stored password' : 'Password'}"/></div>`}<div class="field full"><label>Trusted host fingerprint</label><input id="server-fingerprint" class="input mono" value="${attr(server.hostFingerprint || '')}" readonly placeholder="Filled automatically after Test & trust"/></div></div><div class="notice warning" style="margin-top:12px">${icon('key')}The first connection records the SSH host-key fingerprint. Later deployments fail closed when the server presents a different key.</div></div><footer class="modal-footer">${server.id ? `<button class="button danger" data-action="delete-server" data-server-id="${attr(server.id)}">Delete</button>` : ''}<span class="modal-spacer"></span><button class="button" data-action="close-modal">Cancel</button><button class="button primary" data-action="save-server" data-server-id="${attr(server.id || '')}">Save server</button></footer></section></div>`;
|
|
|
|
|
return `<div class="modal-backdrop" role="presentation"><section class="modal wide-modal" role="dialog" aria-modal="true"><header class="modal-header"><h2>${server.id ? 'Edit' : 'Add'} SSH / Unraid server</h2><button class="icon-button" data-action="close-modal">${icon('close')}</button></header><div class="modal-body"><div class="form-grid"><div class="field"><label>Name</label><input id="server-name" class="input" value="${attr(server.name || 'Unraid')}"/></div><div class="field"><label>Host or IP</label><input id="server-host" class="input" value="${attr(server.host || '')}" placeholder="192.168.1.10"/></div><div class="field"><label>SSH port</label><input id="server-port" class="input" type="number" min="1" max="65535" value="${attr(server.port || 22)}"/></div><div class="field"><label>Username</label><input id="server-username" class="input" value="${attr(server.username || 'root')}"/></div><div class="field"><label>Authentication</label><select id="server-auth-type" class="select"><option value="privateKey" ${authType === 'privateKey' ? 'selected' : ''}>Private key · recommended</option><option value="password" ${authType === 'password' ? 'selected' : ''}>Password</option></select></div><div class="field"><label>Appdata base path</label><input id="server-base-path" class="input" value="${attr(server.basePath || '/mnt/user/appdata')}"/></div>${authType === 'privateKey' ? `<div class="field full"><label>Private key file</label><div class="input-action"><input id="server-private-key" class="input" value="${attr(server.privateKeyPath || '')}" placeholder="C:\\Users\\Jens\\.ssh\\id_ed25519"/><button class="button" data-action="select-private-key">Browse</button></div></div><div class="field full"><label>Private key passphrase</label><input id="server-passphrase" class="input" type="password" placeholder="${server.hasPassphrase ? 'Leave empty to keep stored passphrase' : 'Only when the key is encrypted'}"/></div>` : `<div class="field full"><label>SSH password</label><input id="server-password" class="input" type="password" placeholder="${server.hasPassword ? 'Leave empty to keep stored password' : 'Password'}"/></div>`}<div class="field full"><label>Trusted host fingerprint</label><input id="server-fingerprint" class="input mono" value="${attr(server.hostFingerprint || '')}" readonly placeholder="Filled automatically after Test & trust"/></div></div><div class="notice warning" style="margin-top:12px">${icon('key')}The first connection records the SSH host-key fingerprint. Later deployments fail closed when the server presents a different key.</div></div><footer class="modal-footer">${server.id ? `<button class="button danger" data-action="delete-server" data-server-id="${attr(server.id)}">Delete</button>` : ''}<span class="modal-spacer"></span><button class="button" data-action="close-modal">Cancel</button><button class="button primary" data-action="save-server" data-server-id="${attr(server.id || '')}">Save server</button></footer></section></div>`;
|
|
|
|
|
}
|
|
|
|
|
if (ui.modal.type === 'command-palette') return renderCommandPalette();
|
|
|
|
|
return '';
|
|
|
|
@@ -950,9 +954,11 @@ app.addEventListener('click', async (event) => {
|
|
|
|
|
privateKeyPath: document.querySelector('#server-private-key')?.value.trim() || '',
|
|
|
|
|
hostFingerprint: document.querySelector('#server-fingerprint').value.trim()
|
|
|
|
|
};
|
|
|
|
|
const password = document.querySelector('#server-password')?.value || '';
|
|
|
|
|
const passphrase = document.querySelector('#server-passphrase')?.value || '';
|
|
|
|
|
setLoading(true, 'Saving encrypted SSH configuration…');
|
|
|
|
|
try {
|
|
|
|
|
const result = await window.forgeflow.saveServer(server, document.querySelector('#server-password')?.value || '', document.querySelector('#server-passphrase')?.value || '');
|
|
|
|
|
const result = await window.forgeflow.saveServer(server, password, passphrase);
|
|
|
|
|
ui.boot.state = result.state; ui.modal = null; showToast('Server saved', 'Run Test & trust before creating a deployment.', 'success');
|
|
|
|
|
} catch (error) { showToast('Could not save server', error.message, 'error'); }
|
|
|
|
|
setLoading(false);
|
|
|
|
@@ -973,6 +979,31 @@ app.addEventListener('click', async (event) => {
|
|
|
|
|
else if (action === 'save-roots') { const roots = [...document.querySelectorAll('[data-root-index]')].map((input) => input.value.trim()).filter(Boolean); setLoading(true, 'Saving workspace folders…'); try { ui.boot.state = await window.forgeflow.setWorkspaceRoots(roots); await refreshRepositories(false); showToast('Folders saved', 'Repository discovery has been refreshed.', 'success'); } catch (error) { showToast('Could not save folders', error.message, 'error'); } setLoading(false); }
|
|
|
|
|
else if (action === 'save-gitea-settings') { const baseUrl = document.querySelector('#settings-gitea-url').value.trim(); const token = document.querySelector('#settings-gitea-token').value.trim(); setLoading(true, 'Validating Gitea…'); try { const result = await window.forgeflow.updateGitea({ baseUrl, token }); ui.boot.state = result.state; await refreshRepositories(false); showToast('Gitea connected', `Signed in as ${result.validation.user.login}.`, 'success'); } catch (error) { showToast('Connection failed', error.message, 'error'); } setLoading(false); }
|
|
|
|
|
else if (action === 'save-preferences') { const preferences = { autoRefresh: document.querySelector('#pref-auto-refresh').value === 'true', repositoryPollSeconds: Number(document.querySelector('#pref-repo-poll').value), operationPollSeconds: Number(document.querySelector('#pref-operation-poll').value), preferredCloneProtocol: document.querySelector('#pref-clone-protocol').value }; setLoading(true, 'Saving background settings…'); try { ui.boot.state = await window.forgeflow.setPreferences(preferences); await refreshRepositories(false); showToast('Settings saved', 'Background awareness has been updated.', 'success'); } catch (error) { showToast('Could not save settings', error.message, 'error'); } setLoading(false); }
|
|
|
|
|
else if (action === 'repair-origin') {
|
|
|
|
|
if (!repository?.localPath || !repository.sshUrl) return;
|
|
|
|
|
if (!confirm(`Replace origin with ${repository.sshUrl}? Local files and commits are not changed.`)) return;
|
|
|
|
|
setLoading(true, 'Updating Git origin…');
|
|
|
|
|
try { await window.forgeflow.setOrigin(repository.localPath, repository.sshUrl); await refreshRepositories(false); showToast('Git origin updated', repository.sshUrl, 'success'); }
|
|
|
|
|
catch (error) { showToast('Could not update origin', error.message, 'error'); }
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
else if (action === 'normalize-origins') {
|
|
|
|
|
if (!confirm('Replace legacy origin URLs for every linked repository with the current Gitea SSH URL? Local files and commits are not changed.')) return;
|
|
|
|
|
setLoading(true, 'Normalizing linked Git origins…');
|
|
|
|
|
try {
|
|
|
|
|
const result = await window.forgeflow.normalizeOrigins();
|
|
|
|
|
ui.repositories = result.repositories;
|
|
|
|
|
showToast('Git origins normalized', `${result.changes.length} repository origin${result.changes.length === 1 ? '' : 's'} updated.`, 'success');
|
|
|
|
|
} catch (error) { showToast('Could not normalize origins', error.message, 'error'); }
|
|
|
|
|
setLoading(false); render();
|
|
|
|
|
}
|
|
|
|
|
else if (action === 'repair-index-lock') {
|
|
|
|
|
if (!repository?.localPath || !confirm('Remove the stale Git index.lock for this repository? Only continue after other Git tools have stopped.')) return;
|
|
|
|
|
setLoading(true, 'Repairing stale Git lock…');
|
|
|
|
|
try { await window.forgeflow.repairIndexLock(repository.localPath); await refreshRepositories(false); showToast('Git lock removed', 'The repository can accept Git changes again.', 'success'); }
|
|
|
|
|
catch (error) { showToast('Could not remove Git lock', error.message, 'error'); }
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
else if (action === 'run-system-preflight') await runSystemPreflight();
|
|
|
|
|
else if (action === 'save-diagnostics-preferences') {
|
|
|
|
|
const preferences = {
|
|
|
|
|