feat: US-003 — electron-trpc IPC bridge and appRouter scaffold

- Install electron-trpc, @trpc/server, @trpc/client, @trpc/react-query, @tanstack/react-query, zod
- Create appRouter in src/main/router/index.ts with stub routers for: health, clients, projects, tasks, checkpoints, notes, ai
- health.ping procedure returns 'pong'
- All procedure inputs validated with Zod schemas
- Configure IPC handler in main process (createIPCHandler) and preload (exposeElectronTRPC)
- Create renderer trpc client in src/renderer/lib/trpc.ts with ipcLink
- Wrap app with TRPCProvider + QueryClientProvider in src/renderer/index.tsx
- Verify health.ping in HomePage component (shows 'tRPC IPC bridge: pong')
- Typecheck passes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Roberto Musso
2026-02-19 16:41:52 +01:00
parent b5a9b18be4
commit 5254d13e4e
9 changed files with 338 additions and 78 deletions

View File

@@ -1,14 +1,16 @@
import { app, BrowserWindow } from 'electron';
import path from 'node:path';
import started from 'electron-squirrel-startup';
import { createIPCHandler } from 'electron-trpc/main';
import { initDb } from './db';
import { appRouter } from './router';
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (started) {
app.quit();
}
const createWindow = () => {
const createWindow = (): BrowserWindow => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1280,
@@ -36,6 +38,8 @@ const createWindow = () => {
if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools();
}
return mainWindow;
};
// This method will be called when Electron has finished
@@ -43,7 +47,8 @@ const createWindow = () => {
// Some APIs can only be used after this event occurs.
app.on('ready', () => {
initDb();
createWindow();
const win = createWindow();
createIPCHandler({ router: appRouter, windows: [win] });
});
// Quit when all windows are closed, except on macOS. There, it's common

163
src/main/router/index.ts Normal file
View File

@@ -0,0 +1,163 @@
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
const router = t.router;
const publicProcedure = t.procedure;
// Health router — ping/pong verification
const healthRouter = router({
ping: publicProcedure.query(() => 'pong' as const),
});
// Stub routers — full implementations come in later user stories
const clientsRouter = router({
list: publicProcedure.query(() => []),
create: publicProcedure
.input(z.object({ name: z.string(), parentId: z.string().optional(), industry: z.string().optional() }))
.mutation(() => null),
update: publicProcedure
.input(z.object({ id: z.string(), name: z.string().optional(), industry: z.string().optional() }))
.mutation(() => null),
delete: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(() => null),
deleteWithCascade: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(() => null),
});
const projectsRouter = router({
list: publicProcedure
.input(z.object({ clientId: z.string().optional(), includeArchived: z.boolean().optional() }).optional())
.query(() => []),
listAll: publicProcedure.query(() => [] as Array<{ id: string; name: string }>),
get: publicProcedure
.input(z.object({ id: z.string() }))
.query(() => null),
create: publicProcedure
.input(z.object({ name: z.string(), clientId: z.string().optional() }))
.mutation(() => null),
update: publicProcedure
.input(z.object({
id: z.string(),
name: z.string().optional(),
clientId: z.string().optional(),
status: z.enum(['active', 'archived']).optional(),
aiSummary: z.string().optional(),
}))
.mutation(() => null),
delete: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(() => null),
});
const tasksRouter = router({
list: publicProcedure
.input(z.object({
projectId: z.string().optional(),
status: z.enum(['todo', 'in_progress', 'done']).optional(),
search: z.string().optional(),
orderBy: z.enum(['dueDate', 'priority', 'createdAt']).optional(),
}).optional())
.query(() => []),
create: publicProcedure
.input(z.object({
title: z.string(),
description: z.string().optional(),
status: z.string().optional(),
priority: z.string().optional(),
assignee: z.string().optional(),
dueDate: z.number().optional(),
projectId: z.string().optional(),
}))
.mutation(() => null),
update: publicProcedure
.input(z.object({
id: z.string(),
title: z.string().optional(),
description: z.string().optional(),
status: z.string().optional(),
priority: z.string().optional(),
assignee: z.string().optional(),
dueDate: z.number().optional(),
projectId: z.string().optional(),
}))
.mutation(() => null),
delete: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(() => null),
});
const checkpointsRouter = router({
list: publicProcedure
.input(z.object({ projectId: z.string().optional() }).optional())
.query(() => []),
create: publicProcedure
.input(z.object({
projectId: z.string(),
title: z.string(),
date: z.number(),
isAiSuggested: z.number().optional(),
isApproved: z.number().optional(),
}))
.mutation(() => null),
update: publicProcedure
.input(z.object({
id: z.string(),
title: z.string().optional(),
date: z.number().optional(),
isApproved: z.number().optional(),
}))
.mutation(() => null),
delete: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(() => null),
});
const notesRouter = router({
list: publicProcedure
.input(z.object({ projectId: z.string().optional() }).optional())
.query(() => []),
get: publicProcedure
.input(z.object({ id: z.string() }))
.query(() => null),
create: publicProcedure
.input(z.object({ title: z.string(), content: z.string(), projectId: z.string().optional() }))
.mutation(() => null),
update: publicProcedure
.input(z.object({ id: z.string(), title: z.string().optional(), content: z.string().optional() }))
.mutation(() => null),
delete: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(() => null),
});
const aiRouter = router({
chat: publicProcedure
.input(z.object({
message: z.string(),
context: z.object({
type: z.enum(['global', 'project']),
projectId: z.string().optional(),
}),
}))
.mutation(() => ({ response: '' })),
setToken: publicProcedure
.input(z.object({ token: z.string() }))
.mutation(() => null),
hasToken: publicProcedure.query(() => false),
});
export const appRouter = router({
health: healthRouter,
clients: clientsRouter,
projects: projectsRouter,
tasks: tasksRouter,
checkpoints: checkpointsRouter,
notes: notesRouter,
ai: aiRouter,
});
export type AppRouter = typeof appRouter;