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 = viewDate) {
  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 = viewDate) {
  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 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 = viewDate) {
  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 = viewDate) {
  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 = '';
  }
}

/* ── 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.
document
  .getElementById('parkSection')
  .classList.toggle('collapsed', readCollapseState('parkSection', true));
document.getElementById('parkHeader').addEventListener('click', () => {
  const section = document.getElementById('parkSection');
  section.classList.toggle('collapsed');
  writeCollapseState('parkSection', section.classList.contains('collapsed'));
});

// Pomodoro — default collapsed.
document
  .getElementById('pomoSection')
  .classList.toggle('collapsed', readCollapseState('pomoSection', true));
document.getElementById('pomoHeader').addEventListener('click', () => {
  const section = document.getElementById('pomoSection');
  section.classList.toggle('collapsed');
  writeCollapseState('pomoSection', section.classList.contains('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 = '';
});

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 (activeTimer && activeTimer.paused) resumeTimer();
  else pauseTimer();
});
initHero();
initBoardColumnDnD();
initRapid();
initTodayFlow();
initMonthlyLog();
initMigration();
initSprints();
initTrackers();
initBannerControls();
initLocation();

document.getElementById('prevDay').addEventListener('click', () => {
  viewDate = new Date(viewDate);
  viewDate.setDate(viewDate.getDate() - 1);
  render();
});
document.getElementById('nextDay').addEventListener('click', () => {
  if (isToday(viewDate)) return;
  viewDate = new Date(viewDate);
  viewDate.setDate(viewDate.getDate() + 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 = entries
    .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();
    if (!grouped[key]) {
      order.push(key);
      grouped[key] = { label: e.text, tag: e.tag, totalMs: 0, hasTime: false };
    }
    if (e.tsEnd && e.tsEnd > e.ts) {
      grouped[key].totalMs += e.tsEnd - e.ts;
      grouped[key].hasTime = true;
    }
  });
  const lines = order.map((key) => {
    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: entries,
      categories: categories,
    })
  );
}

saveSnapshot();
setInterval(saveSnapshot, 30 * 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 && activeTimer && !activeTimer.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: entries.length,
    categories: categories.length,
    planTasks: planTasks.length,
    blocks: blocks.length,
    // Runtime state
    timer: activeTimer ? '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);