diff --git a/backend/migrations/core/001_init.js b/backend/migrations/core/001_init.js index 2c24dcaa..654d2e09 100644 --- a/backend/migrations/core/001_init.js +++ b/backend/migrations/core/001_init.js @@ -33,11 +33,34 @@ exports.up = async function(knex) { // Try to save credentials to file, but don't fail if we can't const dataDir = path.join(__dirname, '..', '..', 'data'); const setupInfoPath = path.join(dataDir, 'ADMIN_CREDENTIALS.txt'); - + + // Detect a pending install-from-backup trigger. If one exists, + // these credentials are about to be obsoleted by the restore — + // the backup's admin row replaces this fresh-install one a few + // seconds from now. We still write the file (in case the + // restore fails and the fresh admin is the only way in) but + // annotate the top so admins reading the file after restore + // don't waste time trying credentials that no longer exist. + // Flagged on PR #596 review. + const fsSync = require('fs'); + const backupRoot = process.env.BACKUP_ROOT || '/backup'; + const triggerWillFire = fsSync.existsSync(path.join(backupRoot, 'RESTORE_ON_INSTALL')) + || fsSync.existsSync(path.join(backupRoot, 'RESTORE_ON_INSTALL.txt')); + const restoreNotice = triggerWillFire ? ` + +⚠️ RESTORE_ON_INSTALL TRIGGER DETECTED ⚠️ +These credentials are temporary. An install-from-backup run is queued +to fire on the next server start, which will REPLACE this admin row +with the one from the backup. After the restore completes, log in +with your ORIGINAL pre-disaster credentials — not the ones below. +If the restore fails for some reason, the credentials below remain +valid as a fallback recovery path. +` : ''; + const setupInfo = ` ======================================== PicPeak Admin Credentials -======================================== +========================================${restoreNotice} Your admin account has been created with these credentials: diff --git a/backend/src/services/_installFromBackupBoot.js b/backend/src/services/_installFromBackupBoot.js index 71502585..bbf5f45d 100644 --- a/backend/src/services/_installFromBackupBoot.js +++ b/backend/src/services/_installFromBackupBoot.js @@ -159,6 +159,17 @@ async function isDatabaseFresh(db, logger) { async function tryInstallFromBackup(db, logger) { const log = logger || { info: () => {}, warn: () => {}, error: () => {} }; + // The container's winston logger writes to `/app/logs/combined.log` + // by default and may not always tee to stdout, so admins running + // `docker logs picpeak-beta-backend` after a `compose up` would + // see no signal that a restore happened — flagged on PR #596 + // review. We mirror the key trigger / start / end lines to + // console.log as well so the docker-logs surface tells the story + // without needing to exec into the container. + const announce = (msg) => { + try { console.log(`[install-from-backup] ${msg}`); } catch (_) { /* defensive */ } + }; + const backupRoot = process.env.BACKUP_ROOT || '/backup'; if (!fs.existsSync(backupRoot)) { return { ran: false }; @@ -170,15 +181,18 @@ async function tryInstallFromBackup(db, logger) { } log.info(`Install-from-backup: trigger file found at ${trigger.triggerPath}, target manifest ${trigger.manifestPath}`); + announce(`trigger file detected → ${trigger.manifestPath}`); const forceOverride = process.env.INSTALL_FROM_BACKUP_FORCE === 'true'; const isFresh = await isDatabaseFresh(db, log); if (!isFresh && !forceOverride) { log.warn('Install-from-backup: skipping. Trigger file left in place so you can correct + retry.'); + announce('skipping — install has existing data and INSTALL_FROM_BACKUP_FORCE is not set'); return { ran: false, error: 'Database not empty' }; } log.info(`Install-from-backup: restoring from ${trigger.manifestPath}...`); + announce(`starting restore from ${trigger.manifestPath}`); try { const { restoreService } = require('./restoreService'); @@ -206,6 +220,7 @@ async function tryInstallFromBackup(db, logger) { } log.info(`Install-from-backup: restore completed successfully from ${trigger.manifestPath}`); + announce('restore completed successfully'); // Remove the trigger so the next boot doesn't redo it. try { @@ -219,6 +234,7 @@ async function tryInstallFromBackup(db, logger) { } catch (err) { log.error(`Install-from-backup: FAILED — ${err.message}`); log.warn('Trigger file left in place so you can fix the input and retry by restarting the container.'); + announce(`FAILED — ${err.message}. Trigger file left in place for retry.`); return { ran: false, error: err.message }; } }