Source: 07-lifecycle.js

/* ── Start of day ── */

/**
 * Returns the localStorage key for a given day's start-of-day timestamp.
 * @param {Date} [day=viewDate] - Day to key by; defaults to the day in view so
 *   the session chip reflects whichever day the user has navigated to.
 * @returns {string} Key in the format `wl_sod_YYYY-MM-DD`.
 */
function sodKey(day = getViewDate()) {
  return 'wl_sod_' + dk(day);
}
/**
 * Retrieves a given day's recorded start-of-day timestamp from localStorage.
 * @param {Date} [day=viewDate] - Day to read; defaults to the day in view.
 * @returns {number|null} Unix timestamp (ms), or null if not yet set.
 */
function getDayStart(day = getViewDate()) {
  return parseInt(localStorage.getItem(sodKey(day)) || '0') || null;
}

/**
 * Updates the session chip to show the start time recorded for the day in view
 * (with a green dot), or the default "start the day" label when that day has no
 * recorded start. The button's text content is its accessible name; no static
 * aria-label is used so screen readers announce the current state
 * ("started HH:MM" or "start the day").
 * @returns {void}
 */
function renderSodBtn() {
  const btn = document.getElementById('sodBtn');
  if (!btn) return;
  const sod = getDayStart();
  if (sod) {
    const d = new Date(sod);
    const hh = String(d.getHours()).padStart(2, '0');
    const mm = String(d.getMinutes()).padStart(2, '0');
    btn.textContent = '';
    const dot = document.createElement('span');
    dot.className = 'session-chip__dot';
    dot.setAttribute('aria-hidden', 'true');
    btn.appendChild(dot);
    btn.appendChild(document.createTextNode(`started ${hh}:${mm}`));
  } else {
    btn.textContent = 'start the day';
  }
}

/**
 * Records start-of-day silently (no backup-restore dialog) if the day has not
 * already been started. Called automatically when the first task timer begins.
 * @returns {void}
 */
function ensureDayStarted() {
  // Always records against today: a timer starts "now", regardless of which
  // day the user happens to be viewing.
  const today = new Date();
  if (getDayStart(today)) return;
  localStorage.setItem(sodKey(today), String(Date.now()));
  renderSodBtn();
  renderTimeblock();
}

document.getElementById('sodBtn').addEventListener('click', () => {
  const viewDate = getViewDate();
  const existing = getDayStart();
  if (existing || !isToday(viewDate)) {
    // Set or correct the start time for the day in view. Also the path for
    // back-filling a past day that has no recorded start (no "restore" prompt —
    // that only makes sense for today's first start).
    const base = existing ? new Date(existing) : new Date(viewDate);
    const cur =
      String(base.getHours()).padStart(2, '0') + ':' + String(base.getMinutes()).padStart(2, '0');
    const val = prompt(`Started at (HH:MM):`, cur);
    if (!val) return;
    const [h, m] = val.split(':').map(Number);
    if (isNaN(h) || isNaN(m)) return;
    // Anchor the timestamp to the viewed day's calendar date, not today's.
    const ts = new Date(viewDate);
    ts.setHours(h, m, 0, 0);
    localStorage.setItem(sodKey(), String(ts.getTime()));
    renderSodBtn();
    renderTimeblock();
  } else {
    // First click of today — offer a chance to restore from backup before
    // recording start-of-day. The SOD timestamp is written first so it
    // survives the page reload that importBackup() triggers: the backup only
    // overwrites data keys (entries, tasks, …) and leaves wl_sod_* alone.
    const wantRestore = window.confirm(
      'Start of day — restore data from a backup first?\n\n' +
        'OK     → select a backup file to restore, then start the day\n' +
        'Cancel → start the day now without restoring'
    );
    localStorage.setItem(sodKey(new Date()), String(Date.now()));
    renderSodBtn();
    renderTimeblock();
    if (wantRestore) {
      // Trigger the hidden file input — the change listener calls importBackup()
      document.getElementById('backupFileInput').click();
    }
  }
});

/* ── End of day button state ── */

/**
 * Returns the localStorage key for a given day's end-of-day timestamp.
 * @param {Date} [day=viewDate] - Day to key by; defaults to the day in view.
 * @returns {string} Key in the format `wl_eod_YYYY-MM-DD`.
 */
function eodKey(day = getViewDate()) {
  return 'wl_eod_' + dk(day);
}

/**
 * Retrieves a given day's recorded end-of-day timestamp from localStorage.
 * @param {Date} [day=viewDate] - Day to read; defaults to the day in view.
 * @returns {number|null} Unix timestamp (ms), or null if not yet set.
 */
function getEodTs(day = getViewDate()) {
  return parseInt(localStorage.getItem(eodKey(day)) || '0') || null;
}

/**
 * Updates the "end the day" button to show the end time recorded for the day in
 * view (if any) or its default label, and dims the button once a time is set.
 */
function renderEodBtn() {
  const btn = document.getElementById('eodBtn');
  if (!btn) return;
  const eod = getEodTs();
  if (eod) {
    const d = new Date(eod);
    const hh = String(d.getHours()).padStart(2, '0');
    const mm = String(d.getMinutes()).padStart(2, '0');
    btn.textContent = `🌙 ended ${hh}:${mm}`;
    btn.style.opacity = '0.7';
  } else {
    btn.textContent = '🌙 end the day';
    btn.style.opacity = '';
  }
}

/* ── End of day reminder ── */

// Date key (YYYY-MM-DD) for which the reminder was last dismissed, or null.
// Resets naturally each new day since it's compared against today's key —
// same simplicity as the wipWarnDismissed flag in 10a-tasks-render.js.
let _eodReminderDismissedDate = null;

/**
 * Shows or hides the end-of-day export reminder banner. A nudge, not an
 * automatic export — the app never writes a file without an explicit click,
 * so this only ever asks. Always evaluated against *today*, regardless of
 * which day is currently in view (`viewDate`), since the reminder is about
 * the actual current workday.
 */
function renderEodReminder() {
  const banner = document.getElementById('eodReminderBanner');
  if (!banner) return;
  const today = new Date();
  const todayKey = dk(today);
  const sodTs = getDayStart(today);
  const eodTs = getEodTs(today);
  const hasEntriesToday = getEntries().some((e) => e.date === todayKey);
  const shouldShow =
    _eodReminderDismissedDate !== todayKey &&
    isWorkdayLikelyOver({ sodTs, eodTs, hasEntriesToday, now: Date.now() });
  if (!shouldShow) {
    banner.style.display = 'none';
    return;
  }
  const hours = Math.floor((Date.now() - sodTs) / 3600000);
  const msgEl = document.getElementById('eodReminderMsg');
  if (msgEl) {
    msgEl.textContent = `It's been ${hours}h since you started — looks like your day might be over.`;
  }
  banner.style.display = '';
}

document.getElementById('eodReminderActionBtn')?.addEventListener('click', () => {
  // Reuse the real "end the day" button's click flow verbatim (confirm() gate
  // → openEodModal() → exportTxt()/exportBackup()) — no export logic duplicated here.
  document.getElementById('eodBtn').click();
});
document.getElementById('eodReminderDismissBtn')?.addEventListener('click', () => {
  _eodReminderDismissedDate = dk(new Date());
  renderEodReminder();
});

/* ── Pomodoro weekly clear ── */

/**
 * Clears the pomodoro log when a new ISO week begins.
 * Compares the stored week key against the current ISO week; if they differ,
 * the log is removed and the new week key is saved.
 */
function checkPomoWeeklyClear() {
  const now = new Date();
  const currentWeekKey = `${now.getFullYear()}-W${String(getISOWeek(now)).padStart(2, '0')}`;
  const storedWeek = localStorage.getItem('wl_pomo_week');
  if (storedWeek && storedWeek !== currentWeekKey) {
    localStorage.removeItem(STORE_POMO_LOG);
  }
  localStorage.setItem('wl_pomo_week', currentWeekKey);
}

/* ── Section collapse state persistence ── */

/** localStorage key prefix for section open/collapsed state. */
const COLLAPSE_PREFIX = 'tt-open2-';

/**
 * Reads the stored collapsed state for a section from localStorage.
 * @param {string} sectionId - The section element's `id`.
 * @param {boolean} defaultCollapsed - Value to use when nothing is stored.
 * @returns {boolean} Whether the section should be collapsed.
 */
function readCollapseState(sectionId, defaultCollapsed) {
  const stored = localStorage.getItem(COLLAPSE_PREFIX + sectionId);
  return stored === null ? defaultCollapsed : stored === '1';
}

/**
 * Writes the collapsed state for a section to localStorage.
 * @param {string} sectionId - The section element's `id`.
 * @param {boolean} collapsed - Whether the section is now collapsed.
 * @returns {void}
 */
function writeCollapseState(sectionId, collapsed) {
  localStorage.setItem(COLLAPSE_PREFIX + sectionId, collapsed ? '1' : '0');
}

/* ── Section collapse handlers ── */

// Analytics — default collapsed; state persisted across reloads.
document
  .getElementById('analyticsSection')
  .classList.toggle('collapsed', readCollapseState('analyticsSection', true));
document.getElementById('analyticsHeader').addEventListener('click', () => {
  const section = document.getElementById('analyticsSection');
  section.classList.toggle('collapsed');
  writeCollapseState('analyticsSection', section.classList.contains('collapsed'));
});

// Parked thoughts — default collapsed.
(function () {
  const parkCollapsed = readCollapseState('parkSection', true);
  document.getElementById('parkSection').classList.toggle('collapsed', parkCollapsed);
  document.getElementById('parkHeader').setAttribute('aria-expanded', String(!parkCollapsed));
})();
document.getElementById('parkHeader').addEventListener('click', () => {
  const section = document.getElementById('parkSection');
  section.classList.toggle('collapsed');
  const collapsed = section.classList.contains('collapsed');
  writeCollapseState('parkSection', collapsed);
  document.getElementById('parkHeader').setAttribute('aria-expanded', String(!collapsed));
});

// Pomodoro — default collapsed.
(function () {
  const pomoCollapsed = readCollapseState('pomoSection', true);
  document.getElementById('pomoSection').classList.toggle('collapsed', pomoCollapsed);
  document.getElementById('pomoHeader').setAttribute('aria-expanded', String(!pomoCollapsed));
})();
document.getElementById('pomoHeader').addEventListener('click', () => {
  const section = document.getElementById('pomoSection');
  section.classList.toggle('collapsed');
  const collapsed = section.classList.contains('collapsed');
  writeCollapseState('pomoSection', collapsed);
  document.getElementById('pomoHeader').setAttribute('aria-expanded', String(!collapsed));
});

/* ── Event listeners ── */

// Backup restore: the hidden file input is triggered from the SOD button flow.
// The change handler calls importBackup() and resets the input so the same
// file can be re-selected if needed (e.g. user cancels and retries).
document.getElementById('backupFileInput').addEventListener('change', (e) => {
  const file = e.target.files[0];
  if (file) importBackup(file);
  e.target.value = '';
});

// Merge backup: triggered from the export section's "merge backup" button.
// Adds entries from a backup that are absent in the current store; does not
// replace any existing data.
document.getElementById('mergeBackupBtn').addEventListener('click', () => {
  document.getElementById('mergeFileInput').click();
});
document.getElementById('mergeFileInput').addEventListener('change', (e) => {
  const file = e.target.files[0];
  if (file) mergeBackupEntries(file);
  e.target.value = '';
});

document.getElementById('addBtn').addEventListener('click', () => addEntry(false));
document.getElementById('timerBtn').addEventListener('click', () => addEntry(true));
document.getElementById('captureInput').addEventListener('keydown', (e) => {
  if (e.key === 'Enter') addEntry(false);
});
document.getElementById('timerPause').addEventListener('click', () => {
  if (getActiveTimer() && getActiveTimer().paused) resumeTimer();
  else pauseTimer();
});
initHero();
initBoardColumnDnD();
initBoardTabs();
initRapid();
initTodayFlow();
initMigration();
initSprints();
initTrackers();
initBannerControls();
initLocation();
initWeeklyReport();

/**
 * Moves the viewed day by a whole number of days.
 * Swaps in a fresh Date rather than mutating the current one, so anything
 * still holding the previous viewDate keeps seeing the day it was given.
 * @param {number} dayDelta - Days to move; negative goes back.
 * @returns {void}
 */
function shiftViewDay(dayDelta) {
  const shifted = new Date(getViewDate());
  shifted.setDate(shifted.getDate() + dayDelta);
  setViewDate(shifted);
}

document.getElementById('prevDay').addEventListener('click', () => {
  shiftViewDay(-1);
  render();
});
document.getElementById('nextDay').addEventListener('click', () => {
  if (isToday(getViewDate())) return;
  shiftViewDay(1);
  render();
});

/* ── Auto-backup ── */

/**
 * Writes a recoverable snapshot of today's log entries to localStorage every
 * 30 minutes.  The snapshot contains both a human-readable plaintext summary
 * and the raw entry/category arrays so data can be recovered after accidental
 * clearing.  No-ops when there are no entries for today.
 *
 * Assumption: 30 minutes is an acceptable data-loss window for a personal work
 * log used in a single browser tab. Browser crashes, accidental page reloads,
 * and mis-clicks on "clear data" are the main risks; all are adequately covered
 * by a 30-minute recovery point. If higher durability is needed, reduce the
 * interval in the setInterval call in 07-lifecycle.js.
 */
function saveSnapshot() {
  const todayKey = dk(new Date());
  const dayEntries = getEntries()
    .filter((e) => e.date === todayKey)
    .slice()
    .sort((a, b) => a.ts - b.ts);
  if (!dayEntries.length) return;
  const order = [],
    grouped = {};
  dayEntries.forEach((e) => {
    const key = e.text.toLowerCase();
    // eslint-disable-next-line security/detect-object-injection -- key is an internal app-state value (app data), not from untrusted external input
    if (!grouped[key]) {
      order.push(key);
      // eslint-disable-next-line security/detect-object-injection -- key is an internal app-state value (app data), not from untrusted external input
      grouped[key] = { label: e.text, tag: e.tag, totalMs: 0, hasTime: false };
    }
    if (e.tsEnd && e.tsEnd > e.ts) {
      // eslint-disable-next-line security/detect-object-injection -- key is an internal app-state value (app data), not from untrusted external input
      grouped[key].totalMs += e.tsEnd - e.ts;
      // eslint-disable-next-line security/detect-object-injection -- key is an internal app-state value (app data), not from untrusted external input
      grouped[key].hasTime = true;
    }
  });
  const lines = order.map((key) => {
    // eslint-disable-next-line security/detect-object-injection -- key is an internal app-state value (app data), not from untrusted external input
    const { label, tag, totalMs, hasTime } = grouped[key];
    let timeStr;
    if (hasTime) {
      timeStr = fmtDurLong(totalMs);
    } else {
      timeStr = '--';
    }
    return `${timeStr} - ${label} - ${getCatLabel(tag)}`;
  });
  localStorage.setItem(
    'wl_snapshot',
    JSON.stringify({
      date: todayKey,
      text: lines.join('\n'),
      entries: getEntries(),
      categories: getCategories(),
    })
  );
}

saveSnapshot();
setInterval(saveSnapshot, 30 * 60 * 1000);

// Re-check the end-of-day reminder every 5 minutes so it can appear while the
// user is idle at their desk, without needing some other render() trigger.
setInterval(renderEodReminder, 5 * 60 * 1000);

// Auto-pause when the user switches away (controlled by AUTO_PAUSE_ON_TAB_SWITCH in 00-config.js)
document.addEventListener('visibilitychange', () => {
  if (!AUTO_PAUSE_ON_TAB_SWITCH) return;
  if (document.hidden && getActiveTimer() && !getActiveTimer().paused) {
    pauseTimer();
    wlLog.info('auto-pause: tab hidden while timer running');
  }
});

// Defer config log one tick so `planTasks` (declared in 10-tasks.js, which is
// concatenated after this file) has been initialised before we read its length.
// The IIFE runs all files synchronously; setTimeout(fn, 0) fires after that
// synchronous block completes, so all let/const declarations are in scope.
setTimeout(() => {
  wlLog.config({
    version: '1.8.2',
    date: dk(new Date()),
    // Persistent state counts (from localStorage after load + migration)
    entries: getEntries().length,
    categories: getCategories().length,
    planTasks: getPlanTasks().length,
    blocks: getBlocks().length,
    // Runtime state
    timer: getActiveTimer() ? 'active' : 'idle',
    snapshot: !!localStorage.getItem('wl_snapshot'),
    // Environment: true when the PS API server responded (weather / calendar live)
    apiServer: !!localStorage.getItem('wl_api_ok'),
  });
}, 0);