Source: utils/ephemeralStorage.js

/**
 * @module utils/ephemeralStorage
 * @description Best-effort detection of ephemeral DB storage at boot (INF-006).
 *
 * Render / Fly / Railway free-tier web service filesystems are ephemeral —
 * every redeploy gets a fresh disk and `backend/data/sentri.db` resets to
 * empty, silently wiping accounts, projects, tests, and runs. This module
 * surfaces the symptom as a single visible log line at boot so operators
 * notice the misconfiguration before they lose data on their second deploy.
 *
 * The probe is skipped entirely when `DATABASE_URL` is set (Postgres mode);
 * otherwise it resolves the SQLite DB path the same way the SQLite adapter
 * (`backend/src/database/adapters/sqlite-adapter.js`) does, then writes a
 * `*.boot-marker` file alongside the DB. On a redeploy where the disk
 * survived, the marker's mtime is older than the freshness window and the
 * warning is suppressed; on ephemeral storage the marker is missing and the
 * warning fires.
 *
 * @exports warnIfEphemeralStorage
 */

import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { formatLogLine } from "./logFormatter.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// A marker older than this is treated as proof the filesystem persisted across
// the previous boot. Within the freshness window we assume the marker was
// written by the *current* process, not a prior one, and warn.
const MARKER_FRESHNESS_MS = 10_000;

/**
 * Resolve the default SQLite DB path. Mirrors the SQLite adapter's default at
 * `backend/src/database/adapters/sqlite-adapter.js` so this probe and the
 * adapter never disagree about where the DB lives.
 *
 * @returns {string} Absolute path to the default SQLite DB file.
 */
function defaultDbPath() {
  // backend/src/utils/ephemeralStorage.js → backend/data/sentri.db
  return path.join(__dirname, "..", "..", "data", "sentri.db");
}

/**
 * Emit a warning if the configured SQLite DB path looks ephemeral.
 *
 * No-op (returns early) when `DATABASE_URL` is set — Postgres deployments are
 * always durable, regardless of the host filesystem.
 *
 * @param {Object} [opts]
 * @param {NodeJS.ProcessEnv} [opts.env]    Override env (defaults to `process.env`).
 * @param {Console}           [opts.logger] Override logger (defaults to `console`).
 * @returns {{ warned: boolean, dbPath: string|null }} Outcome for tests/callers.
 */
export function warnIfEphemeralStorage(opts = {}) {
  const env = opts.env || process.env;
  const logger = opts.logger || console;

  if (env.DATABASE_URL) return { warned: false, dbPath: null };

  const rawDbPath = env.DB_PATH || defaultDbPath();
  const dbPath = path.resolve(rawDbPath);
  const markerPath = `${dbPath}.boot-marker`;
  const isTmpPath = dbPath.startsWith("/tmp/") || dbPath === "/tmp";
  let hasPriorProcessWrite = false;

  try {
    const markerStat = fs.statSync(markerPath);
    hasPriorProcessWrite = Date.now() - markerStat.mtimeMs > MARKER_FRESHNESS_MS;
  } catch {
    hasPriorProcessWrite = false;
  }

  const warned = isTmpPath || !hasPriorProcessWrite;
  if (warned) {
    logger.warn(formatLogLine("warn", null, `[db] DB path appears ephemeral — data will be lost on redeploy (path: ${dbPath})`));
  }

  try {
    fs.mkdirSync(path.dirname(markerPath), { recursive: true });
    fs.writeFileSync(markerPath, new Date().toISOString());
  } catch {
    // Best-effort marker write only.
  }

  return { warned, dbPath };
}