wip: app-deploy dependency tracking (incomplete — needs endpoint wiring)

Half-finished feature for declaring and resolving app dependencies
when deploying. Preserved here for later finishing.

What's done:
- app-templates.js: dependsOn declarations on 7 templates
  (sonarr, radarr, lidarr, readarr, bazarr, overseerr, tautulli).
- routes/apps/deploy.js: helper functions checkDependencies(),
  topologicalSortTemplates(), buildDefaultDepConfig().
- routes/recipes/deploy.js: wait-for-health between recipe components
  via appsHelpers.waitForHealthCheck() (verify export exists).
- status/js/app-selector.js: dependency-warning modal injected into
  app-selector flow, with a "deploy with deps" checkbox.

What's missing (blockers for merge):
- POST /api/v1/apps/check-dependencies endpoint — frontend calls it
  (app-selector.js around line 395) but the route is never registered.
  Helper functions exist; just need to expose them. Frontend currently
  404s and falls back to plain deploy (line 401), so the dep-aware
  flow is non-functional.
- Auto-deploy-with-dependencies handler in the modal — checkbox
  exists but nothing wires the "yes deploy them" choice into actually
  deploying the listed dependencies before the target app.
- No tests around topological sort behaviour (circular deps,
  diamond deps, missing deps).

Lifted out of wip/cloud-backups-and-history when the cloud-backups +
resource-history features were merged to main (commit d81d118).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 19:15:42 -07:00
parent d81d1183db
commit 0f2cce362f
4 changed files with 349 additions and 5 deletions

View File

@@ -10,6 +10,22 @@
</div>
</div>`);
injectModal('dep-warning-modal', `<div id="dep-warning-modal" class="weather-modal">
<div class="weather-modal-content" style="min-width: 460px; max-width: 600px;">
<h3 id="dep-warning-title">Missing Dependencies</h3>
<p id="dep-warning-subtitle" class="modal-subtitle"></p>
<div id="dep-warning-list" style="display: flex; flex-direction: column; gap: 8px; margin: 16px 0;"></div>
<p style="font-size: 0.82rem; color: var(--muted); margin-top: 8px;">
Recommended: deploy these dependencies first so the app works correctly on first launch.
</p>
<div class="weather-modal-buttons" style="margin-top: 20px;">
<button id="dep-warning-cancel">Cancel</button>
<button id="dep-warning-skip">Deploy anyway</button>
<button id="dep-warning-confirm" class="btn-accent">Deploy with dependencies</button>
</div>
</div>
</div>`);
injectModal('app-deploy-modal', `<div id="app-deploy-modal" class="weather-modal">
<div class="weather-modal-content" style="min-width: 600px; max-width: 700px;">
<h3 id="app-deploy-title">Deploy Application</h3>
@@ -338,7 +354,7 @@
if (isWidget) {
option.onclick = () => toggleDashboardWidget(app, option);
} else {
option.onclick = () => showDeployConfig(app);
option.onclick = () => checkAndShowDeployConfig(app);
}
grid.appendChild(option);
});
@@ -375,6 +391,93 @@
showNotification(`${app.name} widget ${newState ? 'enabled' : 'disabled'}`, 'success', 2000);
}
// Check declared dependencies for an app before opening the deploy modal.
// If any are missing, show the dep-warning-modal so the user can pick which to auto-deploy.
async function checkAndShowDeployConfig(appTemplate) {
// Reset any leftover dep selection from a previous flow
delete appTemplate._deployDeps;
let depResult = null;
try {
const resp = await secureFetch('/api/v1/apps/check-dependencies', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ appId: appTemplate.id })
});
depResult = await resp.json();
} catch (e) {
// Network/server error — fall through to normal deploy flow
console.warn('Dep check failed, continuing without it:', e);
return showDeployConfig(appTemplate);
}
// No deps declared, or all already deployed
if (!depResult || !Array.isArray(depResult.missing) || depResult.missing.length === 0) {
return showDeployConfig(appTemplate);
}
// Show warning modal
const depModal = document.getElementById('dep-warning-modal');
const titleEl = document.getElementById('dep-warning-title');
const subtitleEl = document.getElementById('dep-warning-subtitle');
const listEl = document.getElementById('dep-warning-list');
const cancelBtn = document.getElementById('dep-warning-cancel');
const skipBtn = document.getElementById('dep-warning-skip');
const confirmBtn = document.getElementById('dep-warning-confirm');
titleEl.textContent = `${appTemplate.name} has missing dependencies`;
subtitleEl.textContent = `${appTemplate.name} works best when these apps are deployed first:`;
// Render checkboxes for each missing dep
listEl.innerHTML = depResult.missingDetails.map(d => `
<label class="radio-option" style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" class="dep-warning-checkbox" value="${escapeHtml(d.id)}" checked style="margin: 0;" />
<span style="font-size: 1.2rem;">${escapeHtml(d.icon || '📦')}</span>
<div>
<div class="fw-500">${escapeHtml(d.name)}</div>
<div class="text-hint">Not deployed</div>
</div>
</label>
`).join('');
// Close the app selector behind it
modal.classList.remove('show');
depModal.classList.add('show');
// Helper to clean up event listeners after one of the buttons is pressed
const cleanup = () => {
cancelBtn.onclick = null;
skipBtn.onclick = null;
confirmBtn.onclick = null;
};
cancelBtn.onclick = () => {
cleanup();
depModal.classList.remove('show');
// Re-open app selector so user can pick something else
modal.classList.add('show');
};
skipBtn.onclick = () => {
cleanup();
depModal.classList.remove('show');
// Continue to normal deploy flow without any deps
showDeployConfig(appTemplate);
};
confirmBtn.onclick = () => {
// Collect checked deps
const chosen = Array.from(listEl.querySelectorAll('.dep-warning-checkbox'))
.filter(cb => cb.checked)
.map(cb => cb.value);
cleanup();
depModal.classList.remove('show');
// Stash chosen deps on the template; addAppToGrid will pick them up
if (chosen.length > 0) appTemplate._deployDeps = chosen;
showDeployConfig(appTemplate);
};
}
// Show deployment configuration modal
async function showDeployConfig(appTemplate) {
const deployModal = document.getElementById('app-deploy-modal');
@@ -765,6 +868,11 @@
}
};
// Include user-selected dependencies (from dep warning modal)
if (Array.isArray(appTemplate._deployDeps) && appTemplate._deployDeps.length > 0) {
apiDeployConfig.deployDependencies = appTemplate._deployDeps;
}
// Add existing container info if using existing
if (usingExisting) {
apiDeployConfig.config.useExisting = true;