fix: rebuild bundle with widget, restore TOTP across container recreate, integrate auto-updater changes
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled

Three logical changes grouped:

1. Widget bundle rebuild + sami-files logo (from previous session)
   - status/dist/{init,core,features,onboarding}.js rebuilt from latest source
   - status/sw.js cache bumped to dashcaddy-shell-594ec75648 to force SW refresh
   - status/assets/sami-files.png added (Sami Files service card logo)

2. status/build.js: include monitoring-widgets.js in bundle
   - The original build.js was missing monitoring-widgets.js from its JS()
     bundle list — that's why the System Overview widget never showed up
     in the live init.js until we ran the live /var/www/dashcaddy-status/
     build.js. Now consistent.

3. dashcaddy-api/scripts/dashcaddy-update.sh restart_container(): preserve
   TOTP secret across container recreates
   - Was only setting SERVICES_FILE; container fell back to image-local
     /app/credentials.json + /app/.encryption-key (auto-generated fresh
     every recreate), which broke TOTP for the bind-mounted secret at
     /app/data/credentials.json
   - Added CREDENTIALS_FILE + ENCRYPTION_KEY_FILE env vars pointing at
     /app/data/ so the container reads from the bind-mounted host data dir
   - See skill: software-development/dashcaddy/references/totp-and-system-overview-pitfalls.md §9

4. Auto-updater integration (pulled from upstream release):
   - dashcaddy-api/VERSION: dev → c64bbe2
   - dashcaddy-api/health-checker.js, middleware.js, package.json,
     routes/backups.js, src/app.js: new release code (bundled workflows,
     /api/auth/ → /api/v1/ back-compat rewrite, backup storage limits)
This commit is contained in:
Hermes
2026-06-18 19:23:30 -07:00
parent 4f377970d7
commit 7bbd969fa2
14 changed files with 844 additions and 359 deletions
+81 -5
View File
@@ -39,6 +39,13 @@ let dockerMaintenance, logDigest;
try { dockerMaintenance = require('../docker-maintenance'); } catch (_) { /* optional module */ }
try { logDigest = require('../log-digest'); } catch (_) { /* optional module */ }
// Workflow engine (bundled workflows)
let bundledWorkflowsModule;
let workflowEngine = null;
try {
bundledWorkflowsModule = require('../bundled-workflows');
} catch (_) { /* optional module */ }
// Templates
const { APP_TEMPLATES, TEMPLATE_CATEGORIES, DIFFICULTY_LEVELS } = require('../app-templates');
const { RECIPE_TEMPLATES, RECIPE_CATEGORIES } = require('../recipe-templates');
@@ -69,6 +76,7 @@ const recipesRoutes = require('../routes/recipes');
const themesRoutes = require('../routes/themes');
const dockerResourcesRoutes = require('../routes/docker-resources');
const eventsRoutes = require('../routes/events');
const workflowsRoutes = require('../routes/workflows');
// Constants
const { APP } = require('../constants');
@@ -156,6 +164,21 @@ async function createApp() {
return null;
}
// Back-compat: reverse-proxy SSO snippets (Caddy forward_auth + per-service
// auto-login pages) historically call these endpoints under the pre-1.5.0
// prefix `/api/auth/...`. The canonical mount is `/api/v1`. Hand-maintained
// Caddyfiles have repeatedly drifted back to the old prefix and 404'd the SSO
// gate (breaking Plex/Jellyfin/Emby/chat). Transparently rewrite ONLY these two
// auth paths to the v1 mount so the gate is tolerant of that drift. Must run
// before configureMiddleware() so CSRF/auth see the canonical path. This is
// deliberately narrow — NOT a general `/api` -> `/api/v1` alias.
app.use((req, res, next) => {
if (req.url.startsWith('/api/auth/gate/') || req.url.startsWith('/api/auth/app-token/')) {
req.url = '/api/v1' + req.url.slice(4); // '/api'.length === 4
}
next();
});
// Configure middleware
const middlewareResult = configureMiddleware(app, {
siteConfig: config.siteConfig,
@@ -308,9 +331,55 @@ async function createApp() {
app,
});
// Initialize workflow engine if bundled-workflows is available
if (bundledWorkflowsModule && ctx.docker) {
try {
const { WorkflowEngine } = bundledWorkflowsModule;
const workflowCtx = {
docker: ctx.docker,
notification: ctx.notification,
backupManager: ctx.backupManager,
resourceMonitor: ctx.resourceMonitor,
servicesStateManager: ctx.servicesStateManager
};
workflowEngine = new WorkflowEngine(workflowCtx);
ctx.workflowEngine = workflowEngine;
log.info('app', 'Workflow engine initialized');
} catch (err) {
log.error('app', 'Failed to initialize workflow engine', { error: err.message });
}
}
// Build versioned API router
const apiRouter = express.Router();
// Wire up notification listeners for resourceMonitor and backupManager
if (ctx.notification && ctx.resourceMonitor) {
ctx.resourceMonitor.on('alert', (alertData) => {
ctx.notification.sendAlert(alertData).catch(err => {
log.error('notification', 'Failed to send alert', { error: err.message });
});
});
ctx.resourceMonitor.on('auto-restart', (data) => {
ctx.notification.sendServiceEvent('auto-restart', data).catch(err => {
log.error('notification', 'Failed to send auto-restart notification', { error: err.message });
});
});
}
if (ctx.notification && ctx.backupManager) {
ctx.backupManager.on('backup-complete', (data) => {
ctx.notification.send('backup-complete', data).catch(err => {
log.error('notification', 'Failed to send backup-complete', { error: err.message });
});
});
ctx.backupManager.on('backup-failed', (data) => {
ctx.notification.send('backup-failed', data).catch(err => {
log.error('notification', 'Failed to send backup-failed', { error: err.message });
});
});
}
// Mount route modules
apiRouter.use(authRoutes(ctx));
apiRouter.use(configRoutes(ctx));
@@ -330,7 +399,8 @@ async function createApp() {
apiRouter.use('/containers', containerRoutes({
docker: ctx.docker,
log: ctx.log,
asyncHandler: ctx.asyncHandler
asyncHandler: ctx.asyncHandler,
workflowEngine: ctx.workflowEngine
}));
apiRouter.use(serviceRoutes({
servicesStateManager: ctx.servicesStateManager,
@@ -361,7 +431,8 @@ async function createApp() {
resourceMonitor: ctx.resourceMonitor,
docker: ctx.docker,
asyncHandler: ctx.asyncHandler,
log: ctx.log
log: ctx.log,
notificationManager: ctx.notification
}));
apiRouter.use(updatesRoutes({
updateManager: ctx.updateManager,
@@ -404,8 +475,8 @@ async function createApp() {
}));
apiRouter.use(backupsRoutes({
backupManager: ctx.backupManager,
asyncHandler: ctx.asyncHandler,
licenseManager: ctx.licenseManager
licenseManager: ctx.licenseManager,
asyncHandler: ctx.asyncHandler
}));
apiRouter.use('/ca', caRoutes(ctx));
apiRouter.use(browseRoutes({
@@ -435,6 +506,11 @@ async function createApp() {
updateManager: ctx.updateManager,
logError: ctx.logError
}));
apiRouter.use(workflowsRoutes({
workflowEngine: ctx.workflowEngine,
licenseManager: ctx.licenseManager,
asyncHandler: ctx.asyncHandler
}));
// Inline API routes
apiRouter.get('/health', (req, res) => {