const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require("electron"); const crypto = require("crypto"); const fs = require("fs/promises"); const fsSync = require("fs"); const path = require("path"); const ICAL = require("ical.js"); const TASKS_FILE = "Tasks.md"; const NOTES_FOLDER = "Notes"; const APP_FOLDER = ".all-in-one"; const SETTINGS_FILE = "settings.json"; const CACHE_FILE = "calendar-cache.json"; const CALENDAR_CACHE_TTL_MS = 15 * 60 * 1000; const DEFAULT_DAILY_TEMPLATE = "# {{dateLong}}\n\n## Notes\n\n"; const DEFAULT_THEME_COLORS = { light: { background: "#f6f7fb", surface: "#ffffff", text: "#20232a", mutedText: "#647084", border: "#d9dee8", sidebar: "#24262d", accent: "#0f766e" }, dark: { background: "#0f172a", surface: "#111827", text: "#e5edf5", mutedText: "#94a3b8", border: "#273449", sidebar: "#111827", accent: "#0f766e" } }; const DEFAULT_SETTINGS = { calendars: [], dailyNoteTemplate: DEFAULT_DAILY_TEMPLATE, theme: "light", themeColors: DEFAULT_THEME_COLORS }; let mainWindow; let state = {}; let vaultPath = ""; function userStatePath() { return path.join(app.getPath("userData"), "state.json"); } function defaultVaultPath() { return path.join(app.getPath("documents"), "All-in-One Vault"); } function appVaultPath(...segments) { return path.join(vaultPath, APP_FOLDER, ...segments); } async function readJson(filePath, fallback) { try { const raw = await fs.readFile(filePath, "utf8"); return JSON.parse(raw); } catch { return fallback; } } async function writeJson(filePath, data) { await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8"); } async function loadState() { state = await readJson(userStatePath(), {}); vaultPath = state.vaultPath || defaultVaultPath(); state.vaultPath = vaultPath; await writeJson(userStatePath(), state); } async function saveState(patch) { state = { ...state, ...patch }; vaultPath = state.vaultPath; await writeJson(userStatePath(), state); } async function ensureFile(filePath, content) { if (!fsSync.existsSync(filePath)) { await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, content, "utf8"); } } async function ensureVault() { await fs.mkdir(vaultPath, { recursive: true }); await fs.mkdir(path.join(vaultPath, "Daily"), { recursive: true }); await fs.mkdir(path.join(vaultPath, NOTES_FOLDER), { recursive: true }); await fs.mkdir(appVaultPath(), { recursive: true }); await ensureFile(path.join(vaultPath, TASKS_FILE), "# Tasks\n\n"); await ensureFile(appVaultPath(SETTINGS_FILE), `${JSON.stringify(DEFAULT_SETTINGS, null, 2)}\n`); } function dateFromISO(dateISO) { const [year, month, day] = dateISO.split("-").map(Number); return new Date(year, month - 1, day); } function toISODate(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; } function addDays(date, days) { const next = new Date(date); next.setDate(next.getDate() + days); return next; } function dayBounds(dateISO) { const start = dateFromISO(dateISO); const end = addDays(start, 1); return { start, end }; } function rangeBounds(startISO, endISO) { const start = dateFromISO(startISO); const end = addDays(dateFromISO(endISO), 1); return { start, end }; } function dateLabel(dateISO) { return new Intl.DateTimeFormat(undefined, { weekday: "long", month: "long", day: "numeric", year: "numeric" }).format(dateFromISO(dateISO)); } function weekdayLabel(dateISO) { return new Intl.DateTimeFormat(undefined, { weekday: "long" }).format(dateFromISO(dateISO)); } function dailyNotePath(dateISO) { return path.join(vaultPath, "Daily", `${dateISO}.md`); } function normalizeTags(input) { const raw = Array.isArray(input) ? input : String(input || "").split(","); const seen = new Set(); return raw .map((tag) => String(tag || "") .trim() .replace(/^#+/, "") .replace(/\s+/g, "-") .slice(0, 48) ) .filter(Boolean) .filter((tag) => { const key = tag.toLowerCase(); if (seen.has(key)) return false; seen.add(key); return true; }); } function normalizeProject(value) { return String(value || "").replace(/\s+/g, " ").trim().slice(0, 100); } function stripNoteFrontmatter(content) { const match = String(content || "").match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/); return match ? content.slice(match[0].length) : content; } function parseNoteFrontmatter(content) { const text = String(content || ""); const metadata = { tags: [], project: "" }; const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); if (!match) { return { metadata, body: text }; } const lines = match[1].split(/\r?\n/); for (let index = 0; index < lines.length; index += 1) { const line = lines[index]; const tagsInline = line.match(/^tags:\s*\[(.*)]\s*$/); const tagsValue = line.match(/^tags:\s*(.+)\s*$/); const projectValue = line.match(/^project:\s*(.*)\s*$/); if (tagsInline) { metadata.tags = normalizeTags(tagsInline[1]); continue; } if (tagsValue && tagsValue[1].trim()) { metadata.tags = normalizeTags(tagsValue[1]); continue; } if (line.match(/^tags:\s*$/)) { const tags = []; while (lines[index + 1]?.match(/^\s*-\s+/)) { index += 1; tags.push(lines[index].replace(/^\s*-\s+/, "")); } metadata.tags = normalizeTags(tags); continue; } if (projectValue) { metadata.project = normalizeProject(projectValue[1].replace(/^["']|["']$/g, "")); } } return { metadata, body: text.slice(match[0].length) }; } function serializeNoteFrontmatter(metadata) { const tags = normalizeTags(metadata.tags); const project = normalizeProject(metadata.project); const lines = ["---"]; if (tags.length) { lines.push("tags:"); tags.forEach((tag) => lines.push(` - ${tag}`)); } if (project) { lines.push(`project: ${project}`); } lines.push("---"); return `${lines.join("\n")}\n`; } function applyNoteMetadata(content, metadata) { const parsed = parseNoteFrontmatter(content); const next = { tags: Object.prototype.hasOwnProperty.call(metadata, "tags") ? normalizeTags(metadata.tags) : parsed.metadata.tags, project: Object.prototype.hasOwnProperty.call(metadata, "project") ? normalizeProject(metadata.project) : parsed.metadata.project }; if (!next.tags.length && !next.project) { return parsed.body; } return `${serializeNoteFrontmatter(next)}${parsed.body.replace(/^\s+/, "")}`; } function renderDailyTemplate(template, dateISO) { return String(template || DEFAULT_DAILY_TEMPLATE) .replaceAll("{{date}}", dateISO) .replaceAll("{{dateLong}}", dateLabel(dateISO)) .replaceAll("{{weekday}}", weekdayLabel(dateISO)); } async function readDailyNote(dateISO) { const filePath = dailyNotePath(dateISO); const settings = await getSettings(); await ensureFile(filePath, renderDailyTemplate(settings.dailyNoteTemplate, dateISO)); const content = await fs.readFile(filePath, "utf8"); const parsed = parseNoteFrontmatter(content); return { dateISO, filePath, content, tags: parsed.metadata.tags, project: parsed.metadata.project }; } async function saveDailyNote(dateISO, content) { const filePath = dailyNotePath(dateISO); await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, content, "utf8"); return readDailyNote(dateISO); } async function updateDailyNoteMetadata(dateISO, metadata) { const filePath = dailyNotePath(dateISO); const current = (await readDailyNote(dateISO)).content; await fs.writeFile(filePath, applyNoteMetadata(current, metadata), "utf8"); return readDailyNote(dateISO); } function generalNotesPath(...segments) { return path.join(vaultPath, NOTES_FOLDER, ...segments); } function noteTitleFromContent(content, fallback) { const heading = stripNoteFrontmatter(content) .split(/\r?\n/) .find((line) => line.trim().startsWith("# ")); return (heading ? heading.replace(/^#\s+/, "").trim() : "") || fallback || "Untitled note"; } function slugifyTitle(title) { const slug = String(title || "") .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 48); return slug || "untitled-note"; } function safeGeneralNoteId(id) { const value = String(id || ""); const baseName = path.basename(value); if (value !== baseName || !baseName.toLowerCase().endsWith(".md")) { throw new Error("Invalid note id."); } return baseName; } function publicGeneralNote(id, filePath, content, stats) { const fallbackTitle = path.basename(id, ".md").replace(/-[a-f0-9]{8}$/, "").replace(/-/g, " "); const parsed = parseNoteFrontmatter(content); return { id, title: noteTitleFromContent(content, fallbackTitle), filePath, updatedAt: stats.mtime.toISOString(), tags: parsed.metadata.tags, project: parsed.metadata.project }; } async function listGeneralNotes() { await fs.mkdir(generalNotesPath(), { recursive: true }); const entries = await fs.readdir(generalNotesPath(), { withFileTypes: true }); const notes = []; for (const entry of entries) { if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".md")) continue; const filePath = generalNotesPath(entry.name); const [content, stats] = await Promise.all([fs.readFile(filePath, "utf8"), fs.stat(filePath)]); notes.push(publicGeneralNote(entry.name, filePath, content, stats)); } return notes.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)); } async function readGeneralNote(id) { const noteId = safeGeneralNoteId(id); const filePath = generalNotesPath(noteId); const [content, stats] = await Promise.all([fs.readFile(filePath, "utf8"), fs.stat(filePath)]); return { ...publicGeneralNote(noteId, filePath, content, stats), content }; } async function createGeneralNote(title) { const cleanTitle = String(title || "").trim().slice(0, 120) || "Untitled note"; const id = `${slugifyTitle(cleanTitle)}-${generateId().slice(0, 8)}.md`; const filePath = generalNotesPath(id); await fs.writeFile(filePath, `# ${cleanTitle}\n\n`, "utf8"); return readGeneralNote(id); } async function saveGeneralNote(id, content) { const noteId = safeGeneralNoteId(id); const filePath = generalNotesPath(noteId); await fs.writeFile(filePath, content || "", "utf8"); return readGeneralNote(noteId); } async function updateGeneralNoteMetadata(id, metadata) { const noteId = safeGeneralNoteId(id); const filePath = generalNotesPath(noteId); const current = await fs.readFile(filePath, "utf8"); await fs.writeFile(filePath, applyNoteMetadata(current, metadata), "utf8"); return readGeneralNote(noteId); } function tasksPath() { return path.join(vaultPath, TASKS_FILE); } function generateId() { return crypto.randomBytes(8).toString("hex"); } function parseMeta(rawMeta = "") { return rawMeta .trim() .split(/\s+/) .filter(Boolean) .reduce((meta, part) => { const separator = part.indexOf("="); if (separator > 0) { meta[part.slice(0, separator)] = part.slice(separator + 1); } return meta; }, {}); } function encodeMetaValue(value) { return encodeURIComponent(String(value || "")); } function decodeMetaValue(value) { try { return decodeURIComponent(String(value || "")); } catch { return String(value || ""); } } function cleanTaskTitle(title) { return title .replace(//g, "") .replace(/\s+/g, " ") .trim() .slice(0, 300); } function serializeTask(task) { const checked = task.completed ? "x" : " "; const meta = [`id=${task.id}`, `created=${task.createdAt || new Date().toISOString()}`]; if (task.dueDate) meta.push(`due=${task.dueDate}`); if (task.completedAt) meta.push(`completed=${task.completedAt}`); if (task.tags?.length) meta.push(`tags=${encodeMetaValue(task.tags.join(","))}`); if (task.project) meta.push(`project=${encodeMetaValue(task.project)}`); return `- [${checked}] ${cleanTaskTitle(task.title) || "Untitled task"} `; } function parseTaskLine(line, lineIndex) { const match = line.match(/^(\s*)-\s+\[([ xX])\]\s+(.*?)(?:\s*)?\s*$/); if (!match) return null; const meta = parseMeta(match[4]); const hasId = Boolean(meta.id); return { id: meta.id || generateId(), title: cleanTaskTitle(match[3]) || "Untitled task", completed: match[2].toLowerCase() === "x", dueDate: /^\d{4}-\d{2}-\d{2}$/.test(meta.due || "") ? meta.due : "", createdAt: meta.created || new Date().toISOString(), completedAt: meta.completed || "", tags: normalizeTags(decodeMetaValue(meta.tags || "")), project: normalizeProject(decodeMetaValue(meta.project || "")), lineIndex, needsRewrite: !hasId }; } async function readTaskDocument() { await ensureFile(tasksPath(), "# Tasks\n\n"); const markdown = await fs.readFile(tasksPath(), "utf8"); const lines = markdown.split(/\r?\n/); const tasks = []; let needsRewrite = false; lines.forEach((line, lineIndex) => { const task = parseTaskLine(line, lineIndex); if (task) { tasks.push(task); needsRewrite = needsRewrite || task.needsRewrite; } }); if (needsRewrite) { tasks.forEach((task) => { lines[task.lineIndex] = serializeTask(task); }); await fs.writeFile(tasksPath(), `${lines.join("\n").replace(/\s*$/, "")}\n`, "utf8"); } return { lines, tasks }; } function publicTask(task) { return { id: task.id, title: task.title, completed: task.completed, dueDate: task.dueDate, createdAt: task.createdAt, completedAt: task.completedAt, tags: task.tags || [], project: task.project || "" }; } function sortTasks(tasks) { return [...tasks].sort((a, b) => { if (a.completed !== b.completed) return a.completed ? 1 : -1; if (a.dueDate && b.dueDate && a.dueDate !== b.dueDate) return a.dueDate.localeCompare(b.dueDate); if (a.dueDate && !b.dueDate) return -1; if (!a.dueDate && b.dueDate) return 1; return a.createdAt.localeCompare(b.createdAt); }); } async function listTasks() { const { tasks } = await readTaskDocument(); return sortTasks(tasks.map(publicTask)); } async function addTask(title, dueDate, tags = [], project = "") { const cleanTitle = cleanTaskTitle(title); if (!cleanTitle) throw new Error("Task title is required."); const task = { id: generateId(), title: cleanTitle, completed: false, dueDate: /^\d{4}-\d{2}-\d{2}$/.test(dueDate || "") ? dueDate : "", createdAt: new Date().toISOString(), completedAt: "", tags: normalizeTags(tags), project: normalizeProject(project) }; const markdown = await fs.readFile(tasksPath(), "utf8"); const nextMarkdown = `${markdown.replace(/\s*$/, "")}\n${serializeTask(task)}\n`; await fs.writeFile(tasksPath(), nextMarkdown, "utf8"); return listTasks(); } async function updateTask(id, patch) { const { lines, tasks } = await readTaskDocument(); const task = tasks.find((item) => item.id === id); if (!task) return listTasks(); const hasCompletedPatch = Object.prototype.hasOwnProperty.call(patch, "completed"); const completed = hasCompletedPatch ? Boolean(patch.completed) : task.completed; const nextTask = { ...task, ...patch, completed, tags: Object.prototype.hasOwnProperty.call(patch, "tags") ? normalizeTags(patch.tags) : task.tags, project: Object.prototype.hasOwnProperty.call(patch, "project") ? normalizeProject(patch.project) : task.project, completedAt: hasCompletedPatch ? (completed ? new Date().toISOString() : "") : task.completedAt }; lines[task.lineIndex] = serializeTask(nextTask); await fs.writeFile(tasksPath(), `${lines.join("\n").replace(/\s*$/, "")}\n`, "utf8"); return listTasks(); } async function deleteTask(id) { const { lines, tasks } = await readTaskDocument(); const task = tasks.find((item) => item.id === id); if (!task) return listTasks(); lines.splice(task.lineIndex, 1); await fs.writeFile(tasksPath(), `${lines.join("\n").replace(/\s*$/, "")}\n`, "utf8"); return listTasks(); } function ensureCalendarUrl(value) { const trimmed = String(value || "").trim(); if (!trimmed) return ""; try { const url = new URL(trimmed); if (!["http:", "https:"].includes(url.protocol)) throw new Error("Invalid protocol"); return url.toString(); } catch { throw new Error("Calendar feed must be a valid http or https URL."); } } function normalizeCalendarSource(calendar, index) { const url = ensureCalendarUrl(calendar?.url || calendar?.calendarFeedUrl || ""); if (!url) return null; return { id: String(calendar?.id || generateId()).replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 32) || generateId(), name: String(calendar?.name || `Calendar ${index + 1}`).replace(/\s+/g, " ").trim().slice(0, 80), url }; } function normalizeSettings(raw) { const migratedCalendars = Array.isArray(raw?.calendars) ? raw.calendars : raw?.calendarFeedUrl ? [{ id: "primary", name: "Google Calendar", url: raw.calendarFeedUrl }] : []; return { calendars: migratedCalendars .map((calendar, index) => normalizeCalendarSource(calendar, index)) .filter(Boolean), dailyNoteTemplate: String(raw?.dailyNoteTemplate || DEFAULT_DAILY_TEMPLATE), theme: raw?.theme === "dark" ? "dark" : "light", themeColors: normalizeThemeColors(raw?.themeColors) }; } function normalizeHexColor(value, fallback) { const color = String(value || "").trim(); return /^#[0-9a-fA-F]{6}$/.test(color) ? color : fallback; } function normalizeThemePalette(raw, defaults) { return Object.fromEntries( Object.entries(defaults).map(([key, fallback]) => [key, normalizeHexColor(raw?.[key], fallback)]) ); } function normalizeThemeColors(raw) { return { light: normalizeThemePalette(raw?.light, DEFAULT_THEME_COLORS.light), dark: normalizeThemePalette(raw?.dark, DEFAULT_THEME_COLORS.dark) }; } async function getSettings() { const raw = await readJson(appVaultPath(SETTINGS_FILE), DEFAULT_SETTINGS); const settings = normalizeSettings(raw); return { ...settings, calendarFeedUrl: settings.calendars[0]?.url || "", vaultPath }; } async function updateSettings(patch) { const current = await getSettings(); const next = normalizeSettings({ calendars: Object.prototype.hasOwnProperty.call(patch, "calendars") ? patch.calendars : current.calendars, calendarFeedUrl: Object.prototype.hasOwnProperty.call(patch, "calendarFeedUrl") ? patch.calendarFeedUrl : current.calendarFeedUrl, dailyNoteTemplate: Object.prototype.hasOwnProperty.call(patch, "dailyNoteTemplate") ? patch.dailyNoteTemplate : current.dailyNoteTemplate, theme: Object.prototype.hasOwnProperty.call(patch, "theme") ? patch.theme : current.theme, themeColors: Object.prototype.hasOwnProperty.call(patch, "themeColors") ? patch.themeColors : current.themeColors }); await writeJson(appVaultPath(SETTINGS_FILE), next); return getSettings(); } function toDate(value) { return value ? value.toJSDate() : null; } function overlaps(start, end, rangeStart, rangeEnd) { return start < rangeEnd && end > rangeStart; } function normalizeCalendarEvent(event, startDate, endDate, occurrenceKey, calendar) { const start = toDate(startDate); const end = toDate(endDate) || addDays(start, startDate.isDate ? 1 : 0); return { id: `${calendar.id}:${event.uid || event.summary || "event"}:${occurrenceKey || start.toISOString()}`, title: event.summary || "Untitled event", startsAt: start.toISOString(), endsAt: end.toISOString(), allDay: Boolean(startDate.isDate), location: event.location || "", description: event.description || "", calendarId: calendar.id, calendarName: calendar.name }; } function parseCalendarEvents(ics, rangeStart, rangeEnd, calendar) { const jcal = ICAL.parse(ics); const component = new ICAL.Component(jcal); const vevents = component.getAllSubcomponents("vevent"); const events = []; for (const vevent of vevents) { const event = new ICAL.Event(vevent); if (event.isRecurring()) { const iterator = event.iterator(); let next; let count = 0; while ((next = iterator.next()) && count < 5000) { count += 1; const details = event.getOccurrenceDetails(next); const start = toDate(details.startDate); const end = toDate(details.endDate) || addDays(start, details.startDate.isDate ? 1 : 0); if (start >= rangeEnd) break; if (overlaps(start, end, rangeStart, rangeEnd)) { events.push(normalizeCalendarEvent(event, details.startDate, details.endDate, next.toString(), calendar)); } } continue; } const start = toDate(event.startDate); const end = toDate(event.endDate) || addDays(start, event.startDate.isDate ? 1 : 0); if (overlaps(start, end, rangeStart, rangeEnd)) { events.push(normalizeCalendarEvent(event, event.startDate, event.endDate, undefined, calendar)); } } return events.sort((a, b) => a.startsAt.localeCompare(b.startsAt)); } async function getCalendarIcs(calendar, force = false) { const cachePath = appVaultPath(CACHE_FILE); const cache = await readJson(cachePath, { calendars: {} }); const cachedCalendar = cache.calendars?.[calendar.id]; const cacheIsFresh = cachedCalendar?.ics && cachedCalendar?.syncedAt && Date.now() - Date.parse(cachedCalendar.syncedAt) < CALENDAR_CACHE_TTL_MS; if (!force && cacheIsFresh) { return { ics: cachedCalendar.ics, syncedAt: cachedCalendar.syncedAt, status: "cached" }; } const response = await fetch(calendar.url); if (!response.ok) { throw new Error(`${calendar.name} returned ${response.status}.`); } const ics = await response.text(); const syncedAt = new Date().toISOString(); await writeJson(cachePath, { calendars: { ...(cache.calendars || {}), [calendar.id]: { ics, syncedAt } } }); return { ics, syncedAt, status: "fresh" }; } async function listCalendarEventsInRange(startISO, endISO, force = false) { const { start, end } = rangeBounds(startISO, endISO); const settings = await getSettings(); if (!settings.calendars.length) { return { events: [], syncedAt: "", status: "not_configured", error: "" }; } const events = []; const syncedAtValues = []; const statuses = []; const errors = []; for (const calendar of settings.calendars) { try { const feed = await getCalendarIcs(calendar, force); syncedAtValues.push(feed.syncedAt); statuses.push(feed.status); events.push(...parseCalendarEvents(feed.ics, start, end, calendar)); } catch (error) { statuses.push("error"); errors.push(error.message || `Could not load ${calendar.name}.`); } } return { events: events.sort((a, b) => a.startsAt.localeCompare(b.startsAt)), syncedAt: syncedAtValues.sort().at(-1) || "", status: errors.length ? "error" : statuses.includes("fresh") ? "fresh" : "cached", error: errors.join(" ") }; } async function listCalendarEvents(dateISO, force = false) { return listCalendarEventsInRange(dateISO, dateISO, force); } async function loadDashboard(dateISO) { await ensureVault(); const [settings, note, generalNotes, tasks, calendar] = await Promise.all([ getSettings(), readDailyNote(dateISO), listGeneralNotes(), listTasks(), listCalendarEvents(dateISO).catch((error) => ({ events: [], syncedAt: "", status: "error", error: error.message || "Could not load calendar." })) ]); return { settings, note, generalNotes, tasks, calendar }; } function createWindow() { Menu.setApplicationMenu(null); mainWindow = new BrowserWindow({ width: 1280, height: 860, minWidth: 960, minHeight: 680, title: "All-in-One Dashboard", autoHideMenuBar: true, backgroundColor: "#f6f7fb", webPreferences: { preload: path.join(__dirname, "preload.cjs"), contextIsolation: true, nodeIntegration: false } }); mainWindow.setMenu(null); mainWindow.setMenuBarVisibility(false); mainWindow.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: "deny" }; }); if (process.env.VITE_DEV_SERVER_URL) { mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL); } else { mainWindow.loadFile(path.join(__dirname, "..", "dist", "index.html")); } } function registerIpc() { ipcMain.handle("dashboard:load", (_event, dateISO) => loadDashboard(dateISO || toISODate(new Date()))); ipcMain.handle("notes:save", (_event, payload) => saveDailyNote(payload.dateISO, payload.content || "")); ipcMain.handle("notes:updateMetadata", (_event, payload) => updateDailyNoteMetadata(payload.dateISO, payload.metadata || {})); ipcMain.handle("generalNotes:create", (_event, title) => createGeneralNote(title)); ipcMain.handle("generalNotes:read", (_event, id) => readGeneralNote(id)); ipcMain.handle("generalNotes:save", (_event, payload) => saveGeneralNote(payload.id, payload.content || "")); ipcMain.handle("generalNotes:updateMetadata", (_event, payload) => updateGeneralNoteMetadata(payload.id, payload.metadata || {}) ); ipcMain.handle("tasks:add", (_event, payload) => addTask(payload.title, payload.dueDate, payload.tags, payload.project)); ipcMain.handle("tasks:toggle", (_event, payload) => updateTask(payload.id, { completed: payload.completed })); ipcMain.handle("tasks:updateMetadata", (_event, payload) => updateTask(payload.id, { tags: payload.tags || [], project: payload.project || "" }) ); ipcMain.handle("tasks:delete", (_event, id) => deleteTask(id)); ipcMain.handle("settings:update", (_event, patch) => updateSettings(patch || {})); ipcMain.handle("calendar:list", (_event, payload) => listCalendarEventsInRange(payload?.startDate || toISODate(new Date()), payload?.endDate || payload?.startDate || toISODate(new Date())) ); ipcMain.handle("calendar:refresh", (_event, payload) => { if (typeof payload === "object" && payload) { return listCalendarEventsInRange(payload.startDate || toISODate(new Date()), payload.endDate || payload.startDate || toISODate(new Date()), true); } return listCalendarEvents(payload || toISODate(new Date()), true); }); ipcMain.handle("vault:open", () => shell.openPath(vaultPath)); ipcMain.handle("vault:choose", async () => { const result = await dialog.showOpenDialog(mainWindow, { title: "Choose Markdown vault", defaultPath: vaultPath, properties: ["openDirectory", "createDirectory"] }); if (result.canceled || !result.filePaths[0]) return getSettings(); await saveState({ vaultPath: result.filePaths[0] }); await ensureVault(); return getSettings(); }); } app.whenReady().then(async () => { await loadState(); await ensureVault(); registerIpc(); createWindow(); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); });