Source: 01b-migrate.js

/**
 * @file 01b-migrate.js
 * Automatic localStorage schema migration.
 *
 * Runs once per page load, before load() reads from the versioned keys.
 * Each migration entry maps an old key name to the new one. If the old
 * key has data and the new key is empty, the data is copied across and
 * the old key is removed. Already-migrated users are unaffected (the
 * old key simply won't exist).
 *
 * Adding a new migration: append an entry to MIGRATIONS. Never remove
 * old entries — a user might be upgrading across multiple versions.
 *
 * @see DATA.md for the full localStorage schema reference.
 */

/**
 * Describes one key-rename migration.
 * @typedef {{ from: string, to: string, description: string }} Migration
 */

/** @type {Migration[]} */
const MIGRATIONS = [
  // v0 → v1: keys gained _v1 suffix and some were renamed for clarity.
  // (Original single-file release used these bare names.)
  { from: 'wl_entries', to: 'wl_entries_v1', description: 'entries array' },
  { from: 'wl_cats', to: 'wl_cats_v1', description: 'categories array' },
  { from: 'wl_categories', to: 'wl_cats_v1', description: 'categories array (alt name)' },
  { from: 'wl_timer', to: 'wl_timer_v1', description: 'active timer state' },
  { from: 'wl_active_timer', to: 'wl_timer_v1', description: 'active timer state (alt name)' },
  { from: 'wl_plan', to: 'wl_plan_v1', description: 'plan tasks array' },
  { from: 'wl_pomoLog', to: 'wl_pomoLog_v1', description: 'pomodoro session log' },
  { from: 'wl_qp_hidden', to: 'wl_qp_hidden_v1', description: 'quick-pick hidden tasks' },
];

/**
 * Runs all pending schema migrations.
 * Safe to call multiple times — migrations are skipped if the source key
 * is absent or the destination key already has data.
 */
function migrateStorage() {
  let migratedCount = 0;
  for (const { from, to, description } of MIGRATIONS) {
    const oldData = localStorage.getItem(from);
    if (!oldData) continue; // already migrated or never existed
    if (localStorage.getItem(to)) {
      // Destination already has data — don't overwrite; just clean up old key
      localStorage.removeItem(from);
      wlLog.warn(
        `migrate: removed stale old key "${from}" (${description}); "${to}" already exists`
      );
      continue;
    }
    localStorage.setItem(to, oldData);
    localStorage.removeItem(from);
    migratedCount++;
    wlLog.info(`migrate: "${from}" → "${to}" (${description})`);
  }
  if (migratedCount > 0) {
    wlLog.info(`migrate: completed ${migratedCount} migration(s)`);
  }
}

/**
 * Fixes entry `date` fields that were stored using UTC midnight instead of the
 * user's local calendar date.
 *
 * Background: `dk()` previously called `toISOString()` (UTC). For UTC+ users
 * (e.g., Finland, UTC+2/+3), entries logged between local midnight and 02:00–03:00
 * were stored under the previous day's UTC date. This migration re-derives `date`
 * from the entry's `ts` timestamp using the now-correct local `dk()`.
 *
 * Safe to run multiple times — entries already on the correct local date are untouched.
 */
function migrateEntryDatesToLocal() {
  const raw = localStorage.getItem('wl_entries_v1');
  if (!raw) return;
  try {
    const all = JSON.parse(raw);
    if (!Array.isArray(all)) return;
    let changed = 0;
    const fixed = all.map((e) => {
      if (typeof e.ts !== 'number' || typeof e.date !== 'string') return e;
      const localDate = dk(new Date(e.ts)); // dk() now returns local date
      if (e.date !== localDate) {
        changed++;
        return { ...e, date: localDate };
      }
      return e;
    });
    if (changed > 0) {
      localStorage.setItem('wl_entries_v1', JSON.stringify(fixed));
      wlLog.info(`migrate: corrected ${changed} entry date(s) from UTC to local calendar date`);
    }
  } catch (e) {
    wlLog.warn('migrate: failed to correct entry dates (UTC → local)', e);
  }
}

// Run immediately so data is in the right keys before load() is called.
migrateStorage();
migrateEntryDatesToLocal();