Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@
"Bash(ls /Users/tomasmaritano/Documents/Github/readied/readide/apps/desktop/src/renderer/components/NoteListFilterBar*)",
"Bash(ls /Users/tomasmaritano/Documents/Github/readied/readide/apps/desktop/src/renderer/components/*.module.css)",
"Bash(npm view:*)",
"Bash(git log:*)"
"Bash(git log:*)",
"Bash(git branch:*)",
"Bash(npx vercel:*)",
"Bash(gh workflow:*)",
"Bash(ls:*)"
Comment thread
tomymaritano marked this conversation as resolved.
]
}
}
19 changes: 17 additions & 2 deletions apps/desktop/electron-vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@ import react from '@vitejs/plugin-react';

export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
plugins: [
externalizeDepsPlugin({
exclude: [
'@readied/core',
'@readied/storage-core',
'@readied/storage-sqlite',
'@readied/sync-core',
'@readied/licensing',
'@readied/ai-core',
],
}),
],
build: {
outDir: 'out/main',
rollupOptions: {
Expand All @@ -18,7 +29,11 @@ export default defineConfig({
},
},
preload: {
plugins: [externalizeDepsPlugin()],
plugins: [
externalizeDepsPlugin({
exclude: ['@readied/core', '@readied/storage-core', '@readied/licensing'],
}),
],
build: {
outDir: 'out/preload',
rollupOptions: {
Expand Down
192 changes: 192 additions & 0 deletions apps/desktop/src/main/handlers/localServerHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/**
* Local HTTP API Server IPC Handlers
*
* Manages the local API server lifecycle and exposes status/token info
* to the renderer (settings UI).
*/

import { ipcMain, app } from 'electron';
import { createNoteId, createNoteOperation, updateNoteOperation } from '@readied/core';
import {
LocalServer,
getOrCreateApiToken,
type LocalServerHandlers,
} from '../services/localServer.js';
import type { SQLiteNoteRepository, DataPaths } from './types.js';

// ============================================================================
// Types
// ============================================================================

export interface LocalServerHandlerDeps {
noteRepository: SQLiteNoteRepository;
dataPaths: DataPaths;
noteToSnapshot: (note: {
id: string;
notebookId: string;
content: string;
title: string;
isPinned: boolean;
isDeleted: boolean;
status: import('@readied/core').NoteStatus;
metadata: {
createdAt: string;
updatedAt: string;
tags: readonly string[];
wordCount: number;
archivedAt: string | null;
};
}) => {
id: string;
notebookId: string;
content: string;
title: string;
createdAt: string;
updatedAt: string;
tags: string[];
wordCount: number;
archivedAt: string | null;
isArchived: boolean;
isPinned: boolean;
isDeleted: boolean;
status: import('@readied/core').NoteStatus;
};
}

// ============================================================================
// Module State
// ============================================================================

const server = new LocalServer();
let apiToken: string | null = null;
Comment thread
tomymaritano marked this conversation as resolved.

// ============================================================================
// Registration
// ============================================================================

export function registerLocalServerHandlers(deps: LocalServerHandlerDeps): void {
const { noteRepository: repo, dataPaths, noteToSnapshot } = deps;

// Build handler callbacks that bridge HTTP requests to the note repository
const handlers: LocalServerHandlers = {
async listNotes() {
const notes = await repo.list();
return notes
.filter(n => !n.isDeleted)
.map(n => ({
id: n.id,
title: n.title,
excerpt: n.content.slice(0, 200).replace(/\n/g, ' '),
updatedAt: n.metadata.updatedAt,
}));
},

async getNote(id) {
const note = await repo.get(createNoteId(id));
if (!note) return null;
const snap = noteToSnapshot(note);
return {
id: snap.id,
title: snap.title,
content: snap.content,
notebookId: snap.notebookId,
createdAt: snap.createdAt,
updatedAt: snap.updatedAt,
tags: snap.tags,
wordCount: snap.wordCount,
isPinned: snap.isPinned,
};
},

async createNote(input) {
const result = await createNoteOperation(input, repo);
if (result.ok) {
return { ok: true, data: { id: result.data.id } };
}
return { ok: false, error: result.error };
},

async updateNote(id, content) {
const noteId = createNoteId(id);
const result = await updateNoteOperation({ id: noteId, content }, repo);
return { ok: result.ok, error: result.ok ? undefined : result.error };
},

async searchNotes(query) {
const notes = await repo.search(query, 50);
return notes.map(n => ({
id: n.id,
title: n.title,
excerpt: n.content.slice(0, 200).replace(/\n/g, ' '),
updatedAt: n.metadata.updatedAt,
}));
},

async getNoteCount() {
return repo.count();
},

getAppVersion() {
return app.getVersion();
},
};

// IPC: Start the local server
ipcMain.handle('localServer:start', async (_event, port?: number) => {
try {
if (port !== undefined && (typeof port !== 'number' || port < 1 || port > 65535)) {
return { ok: false, error: 'Invalid port' };
}
if (server.isRunning()) return { ok: true, port: server.getPort() };
apiToken = await getOrCreateApiToken(dataPaths.root);
await server.start(port, apiToken, handlers);
return { ok: true, port: server.getPort() };
} catch (err) {
return { ok: false, error: err instanceof Error ? err.message : String(err) };
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// IPC: Stop the local server
ipcMain.handle('localServer:stop', async () => {
try {
await server.stop();
return { ok: true };
} catch (err) {
return { ok: false, error: err instanceof Error ? err.message : String(err) };
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// IPC: Get server status
ipcMain.handle('localServer:status', () => {
return {
running: server.isRunning(),
port: server.getPort(),
};
});

// IPC: Get the bearer token (for displaying in settings)
ipcMain.handle('localServer:getToken', async () => {
try {
if (!apiToken) {
apiToken = await getOrCreateApiToken(dataPaths.root);
}
return { ok: true, value: apiToken };
} catch (err) {
return { ok: false, error: err instanceof Error ? err.message : String(err) };
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* Pre-initialise the API bearer token (called from main index).
* The actual server start is controlled by settings — the renderer
* will call localServer:start if the setting is enabled.
*/
export async function initApiToken(dataPaths: DataPaths): Promise<void> {
apiToken = await getOrCreateApiToken(dataPaths.root);
}

/** Stop the server on app quit */
export async function stopLocalServer(): Promise<void> {
await server.stop();
}
Loading
Loading