@knotpad/app 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/brief.js +165 -78
- package/package.json +3 -17
- package/app/(app)/calendar/page.tsx +0 -57
- package/app/(app)/error.tsx +0 -35
- package/app/(app)/graph/page.tsx +0 -32
- package/app/(app)/guide/page.tsx +0 -21
- package/app/(app)/kanban/loading.tsx +0 -24
- package/app/(app)/kanban/page.tsx +0 -59
- package/app/(app)/layout.tsx +0 -122
- package/app/(app)/list/loading.tsx +0 -21
- package/app/(app)/list/page.tsx +0 -137
- package/app/(app)/loading.tsx +0 -18
- package/app/(app)/notes/[noteId]/page.tsx +0 -84
- package/app/(app)/notes/layout.tsx +0 -30
- package/app/(app)/notes/page.tsx +0 -39
- package/app/(app)/page.tsx +0 -5
- package/app/(app)/settings/agent-token/page.tsx +0 -59
- package/app/(app)/settings/backup/page.tsx +0 -49
- package/app/(app)/settings/billing/page.tsx +0 -53
- package/app/(app)/settings/calendar/page.tsx +0 -41
- package/app/(app)/settings/layout.test.tsx +0 -39
- package/app/(app)/settings/layout.tsx +0 -71
- package/app/(app)/settings/page.tsx +0 -4
- package/app/(app)/settings/security/page.tsx +0 -43
- package/app/(app)/settings/team/page.tsx +0 -74
- package/app/(app)/settings/workspace/page.tsx +0 -27
- package/app/(app)/tasks/[taskId]/page.tsx +0 -79
- package/app/(auth)/forgot-password/page.tsx +0 -106
- package/app/(auth)/guest/page.tsx +0 -56
- package/app/(auth)/layout.tsx +0 -13
- package/app/(auth)/login/page.tsx +0 -14
- package/app/(auth)/register/page.tsx +0 -193
- package/app/(auth)/reset-password/page.tsx +0 -138
- package/app/api/account/claim/route.tsx +0 -135
- package/app/api/admin/backfill-encryption/route.tsx +0 -43
- package/app/api/admin/license/route.tsx +0 -42
- package/app/api/auth/2fa/route.tsx +0 -148
- package/app/api/auth/[...nextauth]/route.tsx +0 -3
- package/app/api/auth/change-password/route.tsx +0 -61
- package/app/api/auth/check-2fa/route.tsx +0 -19
- package/app/api/auth/forgot-password/route.tsx +0 -65
- package/app/api/auth/reset-password/route.tsx +0 -52
- package/app/api/auth/verify-2fa/route.tsx +0 -88
- package/app/api/backup/download/db/route.ts +0 -29
- package/app/api/backup/download/notes/route.ts +0 -25
- package/app/api/backup/settings/route.ts +0 -92
- package/app/api/billing/checkout/route.tsx +0 -81
- package/app/api/billing/migrate/route.tsx +0 -163
- package/app/api/billing/portal/route.tsx +0 -24
- package/app/api/billing/setup-intent/route.tsx +0 -55
- package/app/api/billing/status/route.tsx +0 -36
- package/app/api/billing/subscribe/route.tsx +0 -85
- package/app/api/billing/webhook/route.tsx +0 -199
- package/app/api/calendar-feeds/[feedId]/route.tsx +0 -67
- package/app/api/calendar-feeds/[feedId]/sync/route.tsx +0 -37
- package/app/api/calendar-feeds/events/route.tsx +0 -82
- package/app/api/calendar-feeds/route.tsx +0 -52
- package/app/api/calendar-feeds/sync-all/route.tsx +0 -34
- package/app/api/cron/calendar-feeds/route.tsx +0 -31
- package/app/api/cron/stale-tasks/route.tsx +0 -51
- package/app/api/cron/sync/route.tsx +0 -34
- package/app/api/devices/[deviceId]/route.tsx +0 -25
- package/app/api/devices/route.tsx +0 -41
- package/app/api/export/route.tsx +0 -40
- package/app/api/feedback/route.tsx +0 -54
- package/app/api/folders/[folderId]/route.tsx +0 -51
- package/app/api/folders/route.tsx +0 -37
- package/app/api/graph/route.tsx +0 -242
- package/app/api/guest/route.tsx +0 -58
- package/app/api/health/route.tsx +0 -10
- package/app/api/holidays/countries/route.tsx +0 -14
- package/app/api/holidays/route.tsx +0 -49
- package/app/api/holidays/states/route.tsx +0 -21
- package/app/api/invites/[token]/route.tsx +0 -131
- package/app/api/invites/route.tsx +0 -74
- package/app/api/mcp/generate-token/route.tsx +0 -55
- package/app/api/mcp/revoke-token/[tokenId]/route.tsx +0 -30
- package/app/api/mcp/update-alias/[tokenId]/route.tsx +0 -22
- package/app/api/notes/[noteId]/export/route.tsx +0 -45
- package/app/api/notes/[noteId]/route.tsx +0 -360
- package/app/api/notes/route.tsx +0 -112
- package/app/api/notifications/route.tsx +0 -44
- package/app/api/register/route.tsx +0 -67
- package/app/api/restore/route.tsx +0 -148
- package/app/api/sync/conflicts/[conflictId]/route.tsx +0 -134
- package/app/api/sync/conflicts/route.tsx +0 -48
- package/app/api/sync/status/route.tsx +0 -49
- package/app/api/sync/trigger/route.tsx +0 -15
- package/app/api/tasks/[taskId]/detail/route.tsx +0 -68
- package/app/api/tasks/[taskId]/route.tsx +0 -259
- package/app/api/tasks/bulk/route.tsx +0 -133
- package/app/api/tasks/route.tsx +0 -36
- package/app/api/workspace/active/route.tsx +0 -39
- package/app/api/workspace/create-team/route.tsx +0 -42
- package/app/api/workspace/kanban-statuses/route.tsx +0 -71
- package/app/api/workspace/members/[memberId]/route.tsx +0 -69
- package/app/api/workspace/route.tsx +0 -24
- package/app/download/page.tsx +0 -170
- package/app/favicon.ico +0 -0
- package/app/generated/prisma/client.d.ts +0 -1
- package/app/generated/prisma/client.js +0 -5
- package/app/generated/prisma/default.d.ts +0 -1
- package/app/generated/prisma/default.js +0 -5
- package/app/generated/prisma/edge.d.ts +0 -1
- package/app/generated/prisma/edge.js +0 -497
- package/app/generated/prisma/index-browser.js +0 -523
- package/app/generated/prisma/index.d.ts +0 -46376
- package/app/generated/prisma/index.js +0 -497
- package/app/generated/prisma/package.json +0 -144
- package/app/generated/prisma/query_compiler_fast_bg.js +0 -2
- package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
- package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +0 -2
- package/app/generated/prisma/runtime/client.d.ts +0 -3386
- package/app/generated/prisma/runtime/client.js +0 -86
- package/app/generated/prisma/runtime/index-browser.d.ts +0 -90
- package/app/generated/prisma/runtime/index-browser.js +0 -6
- package/app/generated/prisma/runtime/wasm-compiler-edge.js +0 -76
- package/app/generated/prisma/schema.prisma +0 -456
- package/app/generated/prisma/wasm-edge-light-loader.mjs +0 -5
- package/app/generated/prisma/wasm-worker-loader.mjs +0 -5
- package/app/globals.css +0 -54
- package/app/invite/[token]/page.tsx +0 -52
- package/app/layout.tsx +0 -90
- package/app/mcp/route.tsx +0 -430
- package/app/opengraph-image.tsx +0 -120
- package/app/page.tsx +0 -398
- package/app/privacy/page.tsx +0 -69
- package/app/robots.tsx +0 -25
- package/app/sitemap.tsx +0 -36
- package/app/terms/page.tsx +0 -69
- package/app/upgrade/page.tsx +0 -75
- package/auth.config.ts +0 -33
- package/auth.ts +0 -79
- package/components/auth/login-form.tsx +0 -302
- package/components/auth/password-checklist.tsx +0 -31
- package/components/auth/password-input.tsx +0 -36
- package/components/auth/switch-account-button.test.tsx +0 -22
- package/components/auth/switch-account-button.tsx +0 -19
- package/components/auth/two-factor-input.tsx +0 -116
- package/components/billing/billing-dashboard.tsx +0 -265
- package/components/billing/card-form.tsx +0 -210
- package/components/billing/claim-account-form.tsx +0 -99
- package/components/branding/app-logo.test.tsx +0 -20
- package/components/branding/app-logo.tsx +0 -25
- package/components/calendar/calendar-agenda.tsx +0 -150
- package/components/calendar/calendar-drag.test.tsx +0 -177
- package/components/calendar/calendar-grid.tsx +0 -357
- package/components/calendar/calendar-hooks.test.tsx +0 -27
- package/components/calendar/calendar-hooks.ts +0 -351
- package/components/calendar/calendar-toolbar.test.tsx +0 -68
- package/components/calendar/calendar-toolbar.tsx +0 -291
- package/components/calendar/calendar-types.ts +0 -148
- package/components/calendar/calendar-view.test.tsx +0 -295
- package/components/calendar/calendar-view.tsx +0 -307
- package/components/calendar/day-detail-popover.tsx +0 -174
- package/components/calendar/task-chip.tsx +0 -86
- package/components/command/command-palette.test.tsx +0 -33
- package/components/command/command-palette.tsx +0 -310
- package/components/download-cta.tsx +0 -87
- package/components/feedback/feedback-popup.tsx +0 -207
- package/components/graph/graph-draw.ts +0 -337
- package/components/graph/graph-overlays.tsx +0 -160
- package/components/graph/graph-page.test.tsx +0 -131
- package/components/graph/graph-page.tsx +0 -263
- package/components/graph/graph-types.ts +0 -47
- package/components/graph/graph-view.tsx +0 -322
- package/components/guide/guide-view.tsx +0 -522
- package/components/kanban/kanban-board.test.tsx +0 -128
- package/components/kanban/kanban-board.tsx +0 -361
- package/components/kanban/kanban-card-menu.tsx +0 -102
- package/components/kanban/kanban-card.tsx +0 -227
- package/components/kanban/kanban-column.tsx +0 -49
- package/components/kanban/kanban-status-context.tsx +0 -28
- package/components/landing/calendar-sandbox.test.tsx +0 -15
- package/components/landing/calendar-sandbox.tsx +0 -107
- package/components/landing/graph-sandbox.test.tsx +0 -27
- package/components/landing/graph-sandbox.tsx +0 -80
- package/components/landing/kanban-sandbox.test.tsx +0 -24
- package/components/landing/kanban-sandbox.tsx +0 -101
- package/components/landing/landing-showcase.test.tsx +0 -21
- package/components/landing/landing-showcase.tsx +0 -54
- package/components/landing/list-sandbox.tsx +0 -86
- package/components/landing/mock-workspace.ts +0 -168
- package/components/landing/notes-sandbox.test.tsx +0 -14
- package/components/landing/notes-sandbox.tsx +0 -88
- package/components/layout/app-shell.tsx +0 -83
- package/components/layout/backup-scheduler.tsx +0 -122
- package/components/layout/bottom-nav.tsx +0 -43
- package/components/layout/icon-bar.test.tsx +0 -29
- package/components/layout/icon-bar.tsx +0 -118
- package/components/layout/mobile-top-bar.tsx +0 -68
- package/components/layout/notes-panel-folder.tsx +0 -127
- package/components/layout/notes-panel-note-item.tsx +0 -140
- package/components/layout/notes-panel-task-tab.tsx +0 -63
- package/components/layout/notes-panel-types.ts +0 -44
- package/components/layout/notes-panel.tsx +0 -476
- package/components/layout/notification-bell.tsx +0 -251
- package/components/layout/paywall-screen.tsx +0 -41
- package/components/layout/pro-banner.tsx +0 -76
- package/components/layout/sw-register.tsx +0 -27
- package/components/layout/workspace-switcher.tsx +0 -90
- package/components/notes/mobile-bottom-sheet.tsx +0 -99
- package/components/notes/note-editor-context-menu.tsx +0 -47
- package/components/notes/note-editor-dom.ts +0 -33
- package/components/notes/note-editor-dropdowns.tsx +0 -484
- package/components/notes/note-editor-hooks.ts +0 -692
- package/components/notes/note-editor-keyboard.ts +0 -305
- package/components/notes/note-editor-overlay.tsx +0 -90
- package/components/notes/note-editor.test.tsx +0 -372
- package/components/notes/note-editor.tsx +0 -662
- package/components/notes/note-preview-pane.tsx +0 -156
- package/components/notes/note-tabs.tsx +0 -120
- package/components/notes/note-types.tsx +0 -157
- package/components/settings/accept-invite.tsx +0 -108
- package/components/settings/agent-token-settings.tsx +0 -369
- package/components/settings/backup-restore-settings.test.tsx +0 -25
- package/components/settings/backup-restore-settings.tsx +0 -327
- package/components/settings/calendar-feeds-settings.tsx +0 -489
- package/components/settings/calendar-general-settings.tsx +0 -174
- package/components/settings/confirm-danger-action.test.tsx +0 -215
- package/components/settings/confirm-danger-action.tsx +0 -65
- package/components/settings/security-settings.tsx +0 -252
- package/components/settings/settings-guidance.test.tsx +0 -98
- package/components/settings/team-settings.tsx +0 -319
- package/components/settings/two-factor-auth.tsx +0 -296
- package/components/settings/workspace-settings-client.tsx +0 -363
- package/components/settings/workspace-settings-form.tsx +0 -73
- package/components/sync/conflict-viewer.tsx +0 -247
- package/components/sync/sync-indicator.tsx +0 -171
- package/components/tasks/snippet-thread.tsx +0 -119
- package/components/tasks/status-dot.tsx +0 -47
- package/components/tasks/task-badge.tsx +0 -43
- package/components/tasks/task-detail.test.tsx +0 -187
- package/components/tasks/task-detail.tsx +0 -458
- package/components/tasks/task-list-filters.test.tsx +0 -75
- package/components/tasks/task-list-filters.tsx +0 -163
- package/components/tasks/task-list-types.ts +0 -20
- package/components/tasks/task-list.test.tsx +0 -175
- package/components/tasks/task-list.tsx +0 -481
- package/components/tasks/task-row.tsx +0 -85
- package/components/tasks/task-table-row.tsx +0 -259
- package/components/ui/skeleton.tsx +0 -3
- package/components/ui/toast.test.tsx +0 -42
- package/components/ui/toast.tsx +0 -70
- package/electron/main.ts +0 -251
- package/electron/preload.ts +0 -56
- package/instrumentation.tsx +0 -23
- package/lib/api-error.ts +0 -50
- package/lib/backup/backup-runner.test.ts +0 -32
- package/lib/backup/backup-runner.ts +0 -19
- package/lib/backup/backup-schedule.test.ts +0 -23
- package/lib/backup/backup-schedule.ts +0 -55
- package/lib/backup/backup-settings.test.ts +0 -30
- package/lib/backup/backup-settings.ts +0 -27
- package/lib/backup/export-notes-zip.test.ts +0 -26
- package/lib/backup/export-notes-zip.ts +0 -82
- package/lib/backup/export-workspace-backup.test.ts +0 -17
- package/lib/backup/export-workspace-backup.ts +0 -77
- package/lib/backup/restore-workspace-from-export.test.ts +0 -18
- package/lib/backup/restore-workspace-from-export.ts +0 -183
- package/lib/backup/types.ts +0 -14
- package/lib/brand-icons.ts +0 -1
- package/lib/calendar-feed-crypto.ts +0 -38
- package/lib/calendar-feed.ts +0 -239
- package/lib/client/online-status.ts +0 -47
- package/lib/conflict-resolver.test.ts +0 -57
- package/lib/conflict-resolver.ts +0 -240
- package/lib/db-init.ts +0 -79
- package/lib/email.ts +0 -159
- package/lib/encryption.test.ts +0 -41
- package/lib/encryption.ts +0 -98
- package/lib/extract-snippet.test.ts +0 -123
- package/lib/extract-snippet.ts +0 -69
- package/lib/kanban-status.ts +0 -55
- package/lib/license.ts +0 -21
- package/lib/limits.ts +0 -31
- package/lib/mcp-auth.test.ts +0 -58
- package/lib/mcp-auth.ts +0 -65
- package/lib/mcp-contract.test.ts +0 -25
- package/lib/mcp-contract.ts +0 -210
- package/lib/mcp-handler.ts +0 -31
- package/lib/mcp-url.test.ts +0 -12
- package/lib/mcp-url.ts +0 -7
- package/lib/mentions.test.ts +0 -45
- package/lib/mentions.ts +0 -73
- package/lib/note-crypto.ts +0 -108
- package/lib/note-sync.ts +0 -201
- package/lib/note-title.ts +0 -93
- package/lib/prisma.ts +0 -193
- package/lib/pro-flush.ts +0 -292
- package/lib/rate-limit.ts +0 -57
- package/lib/stripe.ts +0 -38
- package/lib/sync-worker.ts +0 -388
- package/lib/task-parser.test.ts +0 -91
- package/lib/task-parser.ts +0 -81
- package/lib/task-utils.ts +0 -52
- package/lib/use-is-electron.ts +0 -19
- package/lib/use-is-mobile.ts +0 -22
- package/lib/validation/calendar-feed.ts +0 -31
- package/lib/validation/note.ts +0 -27
- package/lib/validation/task.ts +0 -26
- package/lib/view-preferences.test.ts +0 -54
- package/lib/view-preferences.ts +0 -28
- package/lib/workspace.ts +0 -66
- package/next.config.ts +0 -21
- package/postcss.config.mjs +0 -7
- package/prisma/migrations/20260519021916_init/migration.sql +0 -388
- package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +0 -8
- package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +0 -2
- package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +0 -12
- package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +0 -3
- package/prisma/migrations/20260529030000_add_folders/migration.sql +0 -17
- package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +0 -31
- package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +0 -5
- package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +0 -14
- package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +0 -26
- package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +0 -23
- package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +0 -43
- package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +0 -2
- package/prisma/migrations/20260611050000_add_task_priority/migration.sql +0 -14
- package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +0 -2
- package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +0 -25
- package/prisma/migrations/20260614160000_add_feedback/migration.sql +0 -20
- package/prisma/migrations/20260614210000_add_2fa/migration.sql +0 -4
- package/prisma/migrations/migration_lock.toml +0 -3
- package/prisma/schema.prisma +0 -457
- package/public/Logo_icon.svg +0 -1
- package/public/file.svg +0 -1
- package/public/globe.svg +0 -1
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +0 -4
- package/public/icon_dark.svg +0 -1
- package/public/knotpad_icon.svg +0 -1
- package/public/knotpad_logo_full.svg +0 -1
- package/public/manifest.json +0 -14
- package/public/next.svg +0 -1
- package/public/sw.js +0 -137
- package/public/vercel.svg +0 -1
- package/public/window.svg +0 -1
- package/tsconfig.json +0 -35
package/electron/main.ts
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Knotpad Desktop — Electron Main Process
|
|
3
|
-
*
|
|
4
|
-
* 1. Finds an available port
|
|
5
|
-
* 2. Spawns the Next.js server (`next start`) as a child process
|
|
6
|
-
* 3. Opens a BrowserWindow pointed at localhost:<port>
|
|
7
|
-
* 4. Handles auto-updater checks (electron-updater)
|
|
8
|
-
* 5. Manages window lifecycle, deep links, and graceful shutdown
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { app, BrowserWindow, dialog, ipcMain, shell } from "electron";
|
|
12
|
-
import { autoUpdater } from "electron-updater";
|
|
13
|
-
import { spawn, ChildProcess } from "child_process";
|
|
14
|
-
import fs from "fs/promises";
|
|
15
|
-
import path from "path";
|
|
16
|
-
import net from "net";
|
|
17
|
-
|
|
18
|
-
// ── Constants ──────────────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
const IS_DEV = !app.isPackaged;
|
|
21
|
-
const PKG_DIR = path.resolve(__dirname, "..");
|
|
22
|
-
|
|
23
|
-
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
function findAvailablePort(startPort = 3000): Promise<number> {
|
|
26
|
-
return new Promise((resolve, reject) => {
|
|
27
|
-
const server = net.createServer();
|
|
28
|
-
server.listen(startPort, () => {
|
|
29
|
-
const port = (server.address() as net.AddressInfo).port;
|
|
30
|
-
server.close(() => resolve(port));
|
|
31
|
-
});
|
|
32
|
-
server.on("error", (err: NodeJS.ErrnoException) => {
|
|
33
|
-
if (err.code === "EADDRINUSE") {
|
|
34
|
-
findAvailablePort(startPort + 1).then(resolve, reject);
|
|
35
|
-
} else {
|
|
36
|
-
reject(err);
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function log(msg: string) {
|
|
43
|
-
console.log(`[electron-main] ${msg}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ── State ────────────────────────────────────────────────────────────────────────
|
|
47
|
-
|
|
48
|
-
let mainWindow: BrowserWindow | null = null;
|
|
49
|
-
let serverProcess: ChildProcess | null = null;
|
|
50
|
-
let serverPort = 0;
|
|
51
|
-
|
|
52
|
-
// ── Next.js Server Lifecycle ─────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
async function startNextJsServer(): Promise<number> {
|
|
55
|
-
const port = await findAvailablePort(3000);
|
|
56
|
-
serverPort = port;
|
|
57
|
-
log(`Starting Next.js server on port ${port}…`);
|
|
58
|
-
|
|
59
|
-
// In dev we use next dev (if dev script is preferred); in prod use next start
|
|
60
|
-
const serverCmd = IS_DEV ? "npx" : path.join(PKG_DIR, "node_modules/.bin/next");
|
|
61
|
-
const serverArgs = IS_DEV
|
|
62
|
-
? ["next", "dev", "--port", String(port)]
|
|
63
|
-
: ["start", "--port", String(port)];
|
|
64
|
-
|
|
65
|
-
serverProcess = spawn(serverCmd, serverArgs, {
|
|
66
|
-
cwd: PKG_DIR,
|
|
67
|
-
stdio: "pipe",
|
|
68
|
-
env: {
|
|
69
|
-
...process.env,
|
|
70
|
-
PORT: String(port),
|
|
71
|
-
// Ensure Electron app uses PGlite local mode by default
|
|
72
|
-
IS_CLOUD: process.env.IS_CLOUD ?? "false",
|
|
73
|
-
NEXT_PUBLIC_APP_URL: `http://localhost:${port}`,
|
|
74
|
-
},
|
|
75
|
-
windowsHide: true,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
serverProcess.stdout?.on("data", (data: Buffer) => {
|
|
79
|
-
process.stdout.write(data);
|
|
80
|
-
});
|
|
81
|
-
serverProcess.stderr?.on("data", (data: Buffer) => {
|
|
82
|
-
process.stderr.write(data);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
serverProcess.on("exit", (code) => {
|
|
86
|
-
log(`Next.js server exited with code ${code ?? "unknown"}`);
|
|
87
|
-
serverProcess = null;
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// Wait a moment for the server to bind, then verify
|
|
91
|
-
await new Promise<void>((resolve) => setTimeout(resolve, 2000));
|
|
92
|
-
return port;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function stopNextJsServer() {
|
|
96
|
-
if (serverProcess) {
|
|
97
|
-
log("Stopping Next.js server…");
|
|
98
|
-
serverProcess.kill("SIGTERM");
|
|
99
|
-
serverProcess = null;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ── Window Management ─────────────────────────────────────────────────────────
|
|
104
|
-
|
|
105
|
-
async function createWindow() {
|
|
106
|
-
mainWindow = new BrowserWindow({
|
|
107
|
-
width: 1400,
|
|
108
|
-
height: 900,
|
|
109
|
-
minWidth: 900,
|
|
110
|
-
minHeight: 600,
|
|
111
|
-
titleBarStyle: "default",
|
|
112
|
-
webPreferences: {
|
|
113
|
-
preload: path.join(__dirname, "preload.js"),
|
|
114
|
-
contextIsolation: true,
|
|
115
|
-
nodeIntegration: false,
|
|
116
|
-
sandbox: true,
|
|
117
|
-
},
|
|
118
|
-
show: false, // show after server is ready
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// Load the Next.js app
|
|
122
|
-
const url = `http://localhost:${serverPort}`;
|
|
123
|
-
log(`Loading ${url}`);
|
|
124
|
-
await mainWindow.loadURL(url);
|
|
125
|
-
|
|
126
|
-
mainWindow.show();
|
|
127
|
-
|
|
128
|
-
if (IS_DEV) {
|
|
129
|
-
mainWindow.webContents.openDevTools();
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Open external links in the OS browser
|
|
133
|
-
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
134
|
-
shell.openExternal(url);
|
|
135
|
-
return { action: "deny" };
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
mainWindow.on("closed", () => {
|
|
139
|
-
mainWindow = null;
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ── Auto Updater ─────────────────────────────────────────────────────────────────
|
|
144
|
-
|
|
145
|
-
function setupAutoUpdater() {
|
|
146
|
-
if (IS_DEV) {
|
|
147
|
-
log("Auto-updater disabled in development");
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
autoUpdater.checkForUpdatesAndNotify();
|
|
152
|
-
|
|
153
|
-
autoUpdater.on("update-available", () => {
|
|
154
|
-
log("Update available — downloading…");
|
|
155
|
-
if (mainWindow) {
|
|
156
|
-
mainWindow.webContents.send("update-available");
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
autoUpdater.on("update-downloaded", () => {
|
|
161
|
-
log("Update downloaded — ready to install");
|
|
162
|
-
if (mainWindow) {
|
|
163
|
-
mainWindow.webContents.send("update-downloaded");
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ── IPC Handlers ───────────────────────────────────────────────────────────────
|
|
169
|
-
|
|
170
|
-
ipcMain.handle("get-app-version", () => app.getVersion());
|
|
171
|
-
|
|
172
|
-
ipcMain.handle("check-for-updates", async () => {
|
|
173
|
-
if (IS_DEV) return { updateAvailable: false };
|
|
174
|
-
const result = await autoUpdater.checkForUpdates();
|
|
175
|
-
return { updateAvailable: result?.updateInfo?.version !== app.getVersion() };
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
ipcMain.handle("install-update", () => {
|
|
179
|
-
autoUpdater.quitAndInstall();
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
ipcMain.handle("get-is-dev", () => IS_DEV);
|
|
183
|
-
|
|
184
|
-
ipcMain.handle("quit-app", () => {
|
|
185
|
-
app.quit();
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
ipcMain.handle("select-backup-folder", async () => {
|
|
189
|
-
const result = await dialog.showOpenDialog({
|
|
190
|
-
properties: ["openDirectory", "createDirectory"],
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
if (result.canceled) return null;
|
|
194
|
-
return result.filePaths[0] ?? null;
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
ipcMain.handle(
|
|
198
|
-
"write-backup-files",
|
|
199
|
-
async (
|
|
200
|
-
_event,
|
|
201
|
-
destinationPath: string,
|
|
202
|
-
files: Array<{ name: string; bytes: number[] }>
|
|
203
|
-
) => {
|
|
204
|
-
await fs.mkdir(destinationPath, { recursive: true });
|
|
205
|
-
|
|
206
|
-
await Promise.all(
|
|
207
|
-
files.map((file) =>
|
|
208
|
-
fs.writeFile(path.join(destinationPath, file.name), Buffer.from(file.bytes))
|
|
209
|
-
)
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
// ── App Lifecycle ──────────────────────────────────────────────────────────────
|
|
215
|
-
|
|
216
|
-
app.whenReady().then(async () => {
|
|
217
|
-
log("App ready — starting Next.js server…");
|
|
218
|
-
await startNextJsServer();
|
|
219
|
-
await createWindow();
|
|
220
|
-
setupAutoUpdater();
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
app.on("window-all-closed", () => {
|
|
224
|
-
stopNextJsServer();
|
|
225
|
-
if (process.platform !== "darwin") {
|
|
226
|
-
app.quit();
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
app.on("activate", async () => {
|
|
231
|
-
if (mainWindow === null) {
|
|
232
|
-
await createWindow();
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
app.on("before-quit", () => {
|
|
237
|
-
stopNextJsServer();
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
// Prevent multiple instances
|
|
241
|
-
const gotTheLock = app.requestSingleInstanceLock();
|
|
242
|
-
if (!gotTheLock) {
|
|
243
|
-
app.quit();
|
|
244
|
-
} else {
|
|
245
|
-
app.on("second-instance", () => {
|
|
246
|
-
if (mainWindow) {
|
|
247
|
-
if (mainWindow.isMinimized()) mainWindow.restore();
|
|
248
|
-
mainWindow.focus();
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
}
|
package/electron/preload.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Knotpad Desktop — Electron Preload Script
|
|
3
|
-
*
|
|
4
|
-
* Securely exposes a minimal API to the renderer process via contextBridge.
|
|
5
|
-
* All IPC calls are typed and validated here.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { contextBridge, ipcRenderer } from "electron";
|
|
9
|
-
|
|
10
|
-
export interface ElectronAPI {
|
|
11
|
-
getAppVersion: () => Promise<string>;
|
|
12
|
-
checkForUpdates: () => Promise<{ updateAvailable: boolean }>;
|
|
13
|
-
installUpdate: () => Promise<void>;
|
|
14
|
-
getIsDev: () => Promise<boolean>;
|
|
15
|
-
quitApp: () => Promise<void>;
|
|
16
|
-
selectBackupFolder: () => Promise<string | null>;
|
|
17
|
-
writeBackupFiles: (
|
|
18
|
-
destinationPath: string,
|
|
19
|
-
files: Array<{ name: string; bytes: number[] }>
|
|
20
|
-
) => Promise<void>;
|
|
21
|
-
onUpdateAvailable: (callback: () => void) => () => void;
|
|
22
|
-
onUpdateDownloaded: (callback: () => void) => () => void;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const api: ElectronAPI = {
|
|
26
|
-
getAppVersion: () => ipcRenderer.invoke("get-app-version"),
|
|
27
|
-
checkForUpdates: () => ipcRenderer.invoke("check-for-updates"),
|
|
28
|
-
installUpdate: () => ipcRenderer.invoke("install-update"),
|
|
29
|
-
getIsDev: () => ipcRenderer.invoke("get-is-dev"),
|
|
30
|
-
quitApp: () => ipcRenderer.invoke("quit-app"),
|
|
31
|
-
selectBackupFolder: () => ipcRenderer.invoke("select-backup-folder"),
|
|
32
|
-
writeBackupFiles: (destinationPath, files) =>
|
|
33
|
-
ipcRenderer.invoke("write-backup-files", destinationPath, files),
|
|
34
|
-
|
|
35
|
-
onUpdateAvailable: (callback: () => void) => {
|
|
36
|
-
const handler = () => callback();
|
|
37
|
-
ipcRenderer.on("update-available", handler);
|
|
38
|
-
return () => ipcRenderer.removeListener("update-available", handler);
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
onUpdateDownloaded: (callback: () => void) => {
|
|
42
|
-
const handler = () => callback();
|
|
43
|
-
ipcRenderer.on("update-downloaded", handler);
|
|
44
|
-
return () => ipcRenderer.removeListener("update-downloaded", handler);
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
contextBridge.exposeInMainWorld("electronAPI", api);
|
|
49
|
-
|
|
50
|
-
// Type augmentation for the renderer
|
|
51
|
-
|
|
52
|
-
declare global {
|
|
53
|
-
interface Window {
|
|
54
|
-
electronAPI: ElectronAPI;
|
|
55
|
-
}
|
|
56
|
-
}
|
package/instrumentation.tsx
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Next.js instrumentation hook — runs once when the server starts,
|
|
3
|
-
* before any request handlers.
|
|
4
|
-
*
|
|
5
|
-
* Local mode (IS_CLOUD=false):
|
|
6
|
-
* 1. Initialise PGlite (async dynamic import + file open)
|
|
7
|
-
* 2. Apply any pending SQL migrations to the local DB
|
|
8
|
-
*
|
|
9
|
-
* Cloud mode (IS_CLOUD=true):
|
|
10
|
-
* No-op. Prisma clients are synchronously initialised at module load,
|
|
11
|
-
* and migrations are applied to Neon by the deploy pipeline.
|
|
12
|
-
*/
|
|
13
|
-
export async function register() {
|
|
14
|
-
if (process.env.NEXT_RUNTIME !== "nodejs") return;
|
|
15
|
-
if (process.env.IS_CLOUD === "true") return;
|
|
16
|
-
|
|
17
|
-
const { initLocalPrisma } = await import("./lib/prisma");
|
|
18
|
-
await initLocalPrisma();
|
|
19
|
-
|
|
20
|
-
const g = global as never as Record<string, unknown>;
|
|
21
|
-
const { runLocalMigrations } = await import("./lib/db-init");
|
|
22
|
-
await runLocalMigrations(g.briefPglite as Parameters<typeof runLocalMigrations>[0]);
|
|
23
|
-
}
|
package/lib/api-error.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import type { z } from "zod";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Standard JSON error shape: `{ error, code? }`. Use across all route handlers
|
|
6
|
-
* so clients can rely on a single contract.
|
|
7
|
-
*/
|
|
8
|
-
export function apiError(message: string, status = 400, code?: string) {
|
|
9
|
-
return NextResponse.json(
|
|
10
|
-
code ? { error: message, code } : { error: message },
|
|
11
|
-
{ status }
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const unauthorized = () => apiError("Unauthorized", 401);
|
|
16
|
-
export const noWorkspace = () => apiError("No workspace", 404);
|
|
17
|
-
export const notFound = (message = "Not found") => apiError(message, 404);
|
|
18
|
-
|
|
19
|
-
type ParseOk<T> = { data: T; response?: undefined };
|
|
20
|
-
type ParseErr = { data?: undefined; response: NextResponse };
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Parse + validate a JSON request body against a zod schema.
|
|
24
|
-
* On failure returns `{ response }` (a ready-to-return 400) so handlers can do:
|
|
25
|
-
*
|
|
26
|
-
* const parsed = await parseJson(req, schema);
|
|
27
|
-
* if (parsed.response) return parsed.response;
|
|
28
|
-
* const { ... } = parsed.data;
|
|
29
|
-
*/
|
|
30
|
-
export async function parseJson<T extends z.ZodTypeAny>(
|
|
31
|
-
req: Request,
|
|
32
|
-
schema: T
|
|
33
|
-
): Promise<ParseOk<z.infer<T>> | ParseErr> {
|
|
34
|
-
let raw: unknown;
|
|
35
|
-
try {
|
|
36
|
-
raw = await req.json();
|
|
37
|
-
} catch {
|
|
38
|
-
return { response: apiError("Invalid JSON body", 400, "INVALID_JSON") };
|
|
39
|
-
}
|
|
40
|
-
const result = schema.safeParse(raw);
|
|
41
|
-
if (!result.success) {
|
|
42
|
-
const first = result.error.issues[0];
|
|
43
|
-
const path = first?.path.join(".");
|
|
44
|
-
const message = first
|
|
45
|
-
? `${path ? path + ": " : ""}${first.message}`
|
|
46
|
-
: "Invalid request";
|
|
47
|
-
return { response: apiError(message, 400, "VALIDATION") };
|
|
48
|
-
}
|
|
49
|
-
return { data: result.data };
|
|
50
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { shouldRunScheduledBackup } from "@/lib/backup/backup-runner";
|
|
3
|
-
|
|
4
|
-
describe("shouldRunScheduledBackup", () => {
|
|
5
|
-
it("runs only when the schedule is enabled and due", () => {
|
|
6
|
-
expect(
|
|
7
|
-
shouldRunScheduledBackup({
|
|
8
|
-
isElectron: true,
|
|
9
|
-
isCloudWorkspace: false,
|
|
10
|
-
scheduleEnabled: true,
|
|
11
|
-
destinationPath: "C:/Backups",
|
|
12
|
-
cadence: "DAILY",
|
|
13
|
-
lastBackupAt: new Date("2026-06-10T08:00:00.000Z"),
|
|
14
|
-
now: new Date("2026-06-11T08:01:00.000Z"),
|
|
15
|
-
})
|
|
16
|
-
).toBe(true);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("skips cloud workspaces", () => {
|
|
20
|
-
expect(
|
|
21
|
-
shouldRunScheduledBackup({
|
|
22
|
-
isElectron: true,
|
|
23
|
-
isCloudWorkspace: true,
|
|
24
|
-
scheduleEnabled: true,
|
|
25
|
-
destinationPath: "C:/Backups",
|
|
26
|
-
cadence: "DAILY",
|
|
27
|
-
lastBackupAt: null,
|
|
28
|
-
now: new Date("2026-06-13T00:00:00.000Z"),
|
|
29
|
-
})
|
|
30
|
-
).toBe(false);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { isBackupDue } from "@/lib/backup/backup-schedule";
|
|
2
|
-
import type { BackupCadence } from "@/lib/backup/types";
|
|
3
|
-
|
|
4
|
-
export function shouldRunScheduledBackup(input: {
|
|
5
|
-
isElectron: boolean;
|
|
6
|
-
isCloudWorkspace: boolean;
|
|
7
|
-
scheduleEnabled: boolean;
|
|
8
|
-
destinationPath: string | null;
|
|
9
|
-
cadence: BackupCadence;
|
|
10
|
-
lastBackupAt: Date | null;
|
|
11
|
-
now?: Date;
|
|
12
|
-
}) {
|
|
13
|
-
if (!input.isElectron) return false;
|
|
14
|
-
if (input.isCloudWorkspace) return false;
|
|
15
|
-
if (!input.scheduleEnabled) return false;
|
|
16
|
-
if (!input.destinationPath) return false;
|
|
17
|
-
|
|
18
|
-
return isBackupDue(input.cadence, input.lastBackupAt, input.now ?? new Date());
|
|
19
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { getNextBackupDueAt, isBackupDue } from "@/lib/backup/backup-schedule";
|
|
3
|
-
|
|
4
|
-
describe("backup schedule cadence", () => {
|
|
5
|
-
it("marks a daily backup due after 24 hours", () => {
|
|
6
|
-
const lastBackupAt = new Date("2026-06-10T08:00:00.000Z");
|
|
7
|
-
const now = new Date("2026-06-11T08:01:00.000Z");
|
|
8
|
-
|
|
9
|
-
expect(isBackupDue("DAILY", lastBackupAt, now)).toBe(true);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("returns the next monthly due date from the last success time", () => {
|
|
13
|
-
const lastBackupAt = new Date("2026-01-31T09:00:00.000Z");
|
|
14
|
-
|
|
15
|
-
expect(getNextBackupDueAt("MONTHLY", lastBackupAt)?.toISOString()).toBe(
|
|
16
|
-
"2026-02-28T09:00:00.000Z"
|
|
17
|
-
);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("treats a missing last backup as immediately due", () => {
|
|
21
|
-
expect(isBackupDue("WEEKLY", null, new Date("2026-06-13T00:00:00.000Z"))).toBe(true);
|
|
22
|
-
});
|
|
23
|
-
});
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type { BackupCadence } from "@/lib/backup/types";
|
|
2
|
-
|
|
3
|
-
function getDaysInMonth(year: number, monthIndex: number) {
|
|
4
|
-
return new Date(Date.UTC(year, monthIndex + 1, 0)).getUTCDate();
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
function addMonthsClamped(date: Date, monthsToAdd: number) {
|
|
8
|
-
const year = date.getUTCFullYear();
|
|
9
|
-
const monthIndex = date.getUTCMonth();
|
|
10
|
-
const day = date.getUTCDate();
|
|
11
|
-
const nextMonthIndex = monthIndex + monthsToAdd;
|
|
12
|
-
const targetYear = year + Math.floor(nextMonthIndex / 12);
|
|
13
|
-
const normalizedMonthIndex = ((nextMonthIndex % 12) + 12) % 12;
|
|
14
|
-
const targetDay = Math.min(day, getDaysInMonth(targetYear, normalizedMonthIndex));
|
|
15
|
-
|
|
16
|
-
return new Date(
|
|
17
|
-
Date.UTC(
|
|
18
|
-
targetYear,
|
|
19
|
-
normalizedMonthIndex,
|
|
20
|
-
targetDay,
|
|
21
|
-
date.getUTCHours(),
|
|
22
|
-
date.getUTCMinutes(),
|
|
23
|
-
date.getUTCSeconds(),
|
|
24
|
-
date.getUTCMilliseconds()
|
|
25
|
-
)
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function getNextBackupDueAt(
|
|
30
|
-
cadence: BackupCadence,
|
|
31
|
-
lastBackupAt: Date | null
|
|
32
|
-
): Date | null {
|
|
33
|
-
if (!lastBackupAt) return null;
|
|
34
|
-
|
|
35
|
-
if (cadence === "DAILY") {
|
|
36
|
-
return new Date(lastBackupAt.getTime() + 24 * 60 * 60 * 1000);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (cadence === "WEEKLY") {
|
|
40
|
-
return new Date(lastBackupAt.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return addMonthsClamped(lastBackupAt, 1);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function isBackupDue(
|
|
47
|
-
cadence: BackupCadence,
|
|
48
|
-
lastBackupAt: Date | null,
|
|
49
|
-
now = new Date()
|
|
50
|
-
): boolean {
|
|
51
|
-
if (!lastBackupAt) return true;
|
|
52
|
-
|
|
53
|
-
const nextDueAt = getNextBackupDueAt(cadence, lastBackupAt);
|
|
54
|
-
return nextDueAt !== null && now >= nextDueAt;
|
|
55
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { validateBackupSettingsInput } from "@/lib/backup/backup-settings";
|
|
3
|
-
|
|
4
|
-
describe("validateBackupSettingsInput", () => {
|
|
5
|
-
it("rejects scheduled backups without a destination path", () => {
|
|
6
|
-
const result = validateBackupSettingsInput({
|
|
7
|
-
scheduleEnabled: true,
|
|
8
|
-
scheduleCadence: "WEEKLY",
|
|
9
|
-
destinationPath: "",
|
|
10
|
-
includeMarkdownZip: false,
|
|
11
|
-
isCloudWorkspace: false,
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
expect(result.success).toBe(false);
|
|
15
|
-
expect(result.error).toBe("Choose a destination folder before enabling scheduled backup.");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("rejects scheduled backup for cloud workspaces", () => {
|
|
19
|
-
const result = validateBackupSettingsInput({
|
|
20
|
-
scheduleEnabled: true,
|
|
21
|
-
scheduleCadence: "DAILY",
|
|
22
|
-
destinationPath: "C:/Backups",
|
|
23
|
-
includeMarkdownZip: true,
|
|
24
|
-
isCloudWorkspace: true,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
expect(result.success).toBe(false);
|
|
28
|
-
expect(result.error).toBe("Scheduled local backup is not available for cloud workspaces.");
|
|
29
|
-
});
|
|
30
|
-
});
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { BackupCadence } from "@/lib/backup/types";
|
|
2
|
-
|
|
3
|
-
type BackupSettingsInput = {
|
|
4
|
-
scheduleEnabled: boolean;
|
|
5
|
-
scheduleCadence: BackupCadence;
|
|
6
|
-
destinationPath: string;
|
|
7
|
-
includeMarkdownZip: boolean;
|
|
8
|
-
isCloudWorkspace: boolean;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export function validateBackupSettingsInput(input: BackupSettingsInput) {
|
|
12
|
-
if (input.scheduleEnabled && input.isCloudWorkspace) {
|
|
13
|
-
return {
|
|
14
|
-
success: false as const,
|
|
15
|
-
error: "Scheduled local backup is not available for cloud workspaces.",
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (input.scheduleEnabled && !input.destinationPath.trim()) {
|
|
20
|
-
return {
|
|
21
|
-
success: false as const,
|
|
22
|
-
error: "Choose a destination folder before enabling scheduled backup.",
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return { success: true as const };
|
|
27
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
listZipEntries,
|
|
4
|
-
slugifyNoteFilename,
|
|
5
|
-
toMarkdownDocument,
|
|
6
|
-
} from "@/lib/backup/export-notes-zip";
|
|
7
|
-
|
|
8
|
-
describe("notes zip export", () => {
|
|
9
|
-
it("wraps note title and content into markdown", () => {
|
|
10
|
-
expect(toMarkdownDocument("Roadmap", "Line 1")).toBe("# Roadmap\n\nLine 1");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("keeps duplicate note filenames unique", () => {
|
|
14
|
-
const entries = listZipEntries([
|
|
15
|
-
{ id: "note_a", title: "Plan", content: "A", folderName: null },
|
|
16
|
-
{ id: "note_b", title: "Plan", content: "B", folderName: null },
|
|
17
|
-
]);
|
|
18
|
-
|
|
19
|
-
expect(entries[0].path).toBe("plan.md");
|
|
20
|
-
expect(entries[1].path).toBe("plan-note_b.md");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("slugifies note filenames safely", () => {
|
|
24
|
-
expect(slugifyNoteFilename("Q3 / Goals")).toBe("q3-goals");
|
|
25
|
-
});
|
|
26
|
-
});
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { strToU8, zipSync } from "fflate";
|
|
2
|
-
import { prisma } from "@/lib/prisma";
|
|
3
|
-
import { decryptContent } from "@/lib/note-crypto";
|
|
4
|
-
import { cleanBackupContent } from "@/lib/backup/export-workspace-backup";
|
|
5
|
-
|
|
6
|
-
type NotesZipEntry = {
|
|
7
|
-
path: string;
|
|
8
|
-
content: string;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
type NotesZipSource = {
|
|
12
|
-
id: string;
|
|
13
|
-
title: string;
|
|
14
|
-
content: string;
|
|
15
|
-
folderName: string | null;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export function slugifyNoteFilename(title: string): string {
|
|
19
|
-
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "note";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function toMarkdownDocument(title: string, content: string): string {
|
|
23
|
-
return `# ${title}\n\n${content}`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function listZipEntries(notes: NotesZipSource[]): NotesZipEntry[] {
|
|
27
|
-
const seen = new Set<string>();
|
|
28
|
-
|
|
29
|
-
return notes.map((note) => {
|
|
30
|
-
const baseName = slugifyNoteFilename(note.title);
|
|
31
|
-
const folderPrefix = note.folderName ? `${slugifyNoteFilename(note.folderName)}/` : "";
|
|
32
|
-
const defaultPath = `${folderPrefix}${baseName}.md`;
|
|
33
|
-
const path = seen.has(defaultPath)
|
|
34
|
-
? `${folderPrefix}${baseName}-${note.id}.md`
|
|
35
|
-
: defaultPath;
|
|
36
|
-
|
|
37
|
-
seen.add(path);
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
path,
|
|
41
|
-
content: toMarkdownDocument(note.title, note.content),
|
|
42
|
-
};
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function buildNotesZip(entries: NotesZipEntry[]): Uint8Array {
|
|
47
|
-
const archive = Object.fromEntries(
|
|
48
|
-
entries.map((entry) => [entry.path, strToU8(entry.content)])
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
return zipSync(archive, { level: 6 });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function buildNotesZipExport(workspaceId: string) {
|
|
55
|
-
const notes = await prisma.note.findMany({
|
|
56
|
-
where: { workspaceId },
|
|
57
|
-
orderBy: { createdAt: "asc" },
|
|
58
|
-
include: {
|
|
59
|
-
folder: {
|
|
60
|
-
select: {
|
|
61
|
-
name: true,
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const entries = listZipEntries(
|
|
68
|
-
await Promise.all(
|
|
69
|
-
notes.map(async (note) => ({
|
|
70
|
-
id: note.id,
|
|
71
|
-
title: note.title,
|
|
72
|
-
content: cleanBackupContent(await decryptContent(note.content, workspaceId)),
|
|
73
|
-
folderName: note.folder?.name ?? null,
|
|
74
|
-
}))
|
|
75
|
-
)
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
filename: `brief-notes-${new Date().toISOString().split("T")[0]}.zip`,
|
|
80
|
-
bytes: buildNotesZip(entries),
|
|
81
|
-
};
|
|
82
|
-
}
|