Source: 01-state.js

const STORE_ENTRIES = 'wl_entries_v1';
const STORE_TIMER = 'wl_timer_v1';
const STORE_POMO_LOG = 'wl_pomoLog_v1';
const STORE_CATS = 'wl_cats_v1';
const STORE_QP_HIDDEN = 'wl_qp_hidden_v1';
const STORE_LOGNOTES = 'wl_lognotes_v1';
const STORE_TRACKERS = 'wl_trackers_v1';
const STORE_MIGRATION = 'wl_migration_v1';
const STORE_LOCATION = 'wl_location_v1';

// Lowercase task texts the user has dismissed from the recent-tasks list
let qpHidden = (() => {
  try {
    const raw = JSON.parse(localStorage.getItem(STORE_QP_HIDDEN) || '[]');
    return new Set(Array.isArray(raw) ? raw.map((s) => String(s).toLowerCase()) : []);
  } catch (e) {
    return new Set();
  }
})();
function saveQpHidden() {
  localStorage.setItem(STORE_QP_HIDDEN, JSON.stringify([...qpHidden]));
}

const DEFAULT_CATS = [
  { id: 'work', label: 'work', color: '#378ADD' },
  { id: 'meeting', label: 'meeting', color: '#7EC8E3' },
  { id: 'focus', label: 'deep focus', color: '#1D9E75' },
  { id: 'break', label: 'break', color: '#BA7517' },
  { id: 'other', label: 'other', color: '#888780' },
];
const DEFAULT_IDS = new Set(DEFAULT_CATS.map((c) => c.id));
const CUSTOM_PALETTE = [
  '#7B61FF',
  '#E67E22',
  '#0d9488',
  '#3F51B5',
  '#16A085',
  '#9B59B6',
  '#F39C12',
  '#00BCD4',
  '#27AE60',
  '#E91E63',
  '#FF5722',
  '#2ECC71',
  '#C0392B',
  '#1E88E5',
  '#43A047',
  '#FB8C00',
  '#8E24AA',
  '#039BE5',
  '#6D4C41',
  '#00897B',
  '#F4511E',
  '#D81B60',
  '#546E7A',
  '#FDD835',
  '#5E35B1',
];

/**
 * Returns the next visually distinct colour from the palette for a new category.
 * Falls back to golden-angle HSL generation once all palette colours are in use.
 * @returns {string} A CSS colour string (hex or hsl).
 */
function nextDistinctColor() {
  const usedColors = new Set(categories.map((c) => c.color.toLowerCase()));
  const pick = CUSTOM_PALETTE.find((c) => !usedColors.has(c.toLowerCase()));
  if (pick) return pick;
  // All palette colours used — generate by golden-angle hue steps
  const hue = (usedColors.size * 137) % 360;
  return `hsl(${hue}, 65%, 52%)`;
}

let viewDate = new Date();
let selectedTag = 'work';
let logNotes = [];
let trackers = [];
let entries = [];
let activeTimer = null;
let timerInterval = null;
let categories = [...DEFAULT_CATS];
let chartMode = 'task';
let blocks = [];
let planDragId = null;

/* ── Load / Save ── */
// Schema validators (validEntry, validCategory, validPlanTask, validBlock, validTimer,
// validPomoEntry) are defined in 00-pure-fns.js (concatenated earlier in the build).

/**
 * Loads all persistent state from localStorage into module-level variables.
 * Invalid records are dropped per-item (rather than rejecting entire arrays)
 * and any drops are reported via wlLog.warn so data-quality issues are visible
 * in DevTools rather than silently disappearing.
 * Falls back to the last snapshot if entries are missing from primary storage.
 */
function load() {
  try {
    const parsedEntries = JSON.parse(localStorage.getItem(STORE_ENTRIES) || '[]');
    const allEntries = Array.isArray(parsedEntries) ? parsedEntries : [];
    entries = allEntries.filter(validEntry);
    if (entries.length < allEntries.length)
      wlLog.warn(`load: dropped ${allEntries.length - entries.length} invalid entry record(s)`, {
        total: allEntries.length,
        kept: entries.length,
      });
  } catch (e) {
    entries = [];
    wlLog.error('load: failed to parse entries from localStorage', e);
  }
  try {
    const parsedTimer = JSON.parse(localStorage.getItem(STORE_TIMER) || 'null');
    activeTimer = parsedTimer && validTimer(parsedTimer) ? parsedTimer : null;
    if (parsedTimer && !validTimer(parsedTimer))
      wlLog.warn('load: discarded invalid timer state', parsedTimer);
  } catch (e) {
    activeTimer = null;
    wlLog.error('load: failed to parse timer state', e);
  }
  try {
    const parsedCategories = JSON.parse(localStorage.getItem(STORE_CATS) || 'null');
    if (Array.isArray(parsedCategories) && parsedCategories.length) {
      categories = parsedCategories.filter(validCategory);
      if (categories.length < parsedCategories.length)
        wlLog.warn(
          `load: dropped ${parsedCategories.length - categories.length} invalid category record(s)`,
          {
            total: parsedCategories.length,
            kept: categories.length,
          }
        );
    }
  } catch (e) {
    wlLog.error('load: failed to parse categories', e);
  }
  // Auto-restore from snapshot if entries are unexpectedly empty
  if (!entries.length) {
    try {
      const snap = JSON.parse(localStorage.getItem('wl_snapshot') || 'null');
      if (snap && Array.isArray(snap.entries) && snap.entries.length) {
        entries = snap.entries.filter(validEntry);
        if (Array.isArray(snap.categories) && snap.categories.length)
          categories = snap.categories.filter(validCategory);
        wlLog.warn('load: restored from snapshot — entries were missing from primary storage');
      }
    } catch (e) {
      wlLog.warn('load: failed to parse snapshot from localStorage', e);
    }
  }
  loadLogNotes();
  loadTrackers();
}

function loadLogNotes() {
  try {
    const raw = JSON.parse(localStorage.getItem(STORE_LOGNOTES) || '[]');
    logNotes = Array.isArray(raw) ? raw : [];
  } catch (e) {
    logNotes = [];
    wlLog.warn('loadLogNotes: failed to parse log notes from localStorage', e);
  }
}

function saveLogNotes() {
  localStorage.setItem(STORE_LOGNOTES, JSON.stringify(logNotes));
}

/**
 * Persists entries, active timer, and categories to localStorage.
 * Refuses to overwrite existing non-empty data with an empty array to guard against
 * accidental data loss if save() is called before load() completes.
 *
 * Assumption: an empty `entries` array in memory while localStorage still holds
 * data means save() was called before load() finished (e.g. a race during init),
 * not that the user intentionally deleted everything. Intentional clearing goes
 * through a dedicated wipe path that bypasses this guard.
 */
function save() {
  // Never overwrite real data with empty arrays
  const existing = localStorage.getItem(STORE_ENTRIES);
  if (!entries.length && existing && existing !== '[]') {
    wlLog.warn('save() blocked — refusing to overwrite existing entries with empty array');
    return;
  }
  localStorage.setItem(STORE_ENTRIES, JSON.stringify(entries));
  localStorage.setItem(STORE_TIMER, JSON.stringify(activeTimer));
  localStorage.setItem(STORE_CATS, JSON.stringify(categories));
}