Implement local SQLite backend and reactive UI
This commit is contained in:
@@ -12,6 +12,8 @@ export type IpcMethod<Input, Output> = {
|
||||
input: z.ZodType<Input>;
|
||||
output: z.ZodType<Output>;
|
||||
handler: (input: Input) => Promise<Output> | Output;
|
||||
rawArgs?: boolean;
|
||||
rawResult?: boolean;
|
||||
};
|
||||
|
||||
function validationDetail(error: z.ZodError): unknown {
|
||||
@@ -22,10 +24,11 @@ function validationDetail(error: z.ZodError): unknown {
|
||||
}
|
||||
|
||||
export function registerIpcMethod<Input, Output>(method: IpcMethod<Input, Output>): void {
|
||||
ipcMain.handle(method.channel, async (_event, rawInput): Promise<IpcResult<Output>> => {
|
||||
ipcMain.handle(method.channel, async (_event, ...rawArgs): Promise<IpcResult<Output> | Output> => {
|
||||
const rawInput = method.rawArgs ? rawArgs : rawArgs[0];
|
||||
const parsedInput = method.input.safeParse(rawInput);
|
||||
if (!parsedInput.success) {
|
||||
return {
|
||||
const errorResult: IpcResult<Output> = {
|
||||
ok: false,
|
||||
error: {
|
||||
code: "VALIDATION_ERROR",
|
||||
@@ -33,29 +36,32 @@ export function registerIpcMethod<Input, Output>(method: IpcMethod<Input, Output
|
||||
detail: validationDetail(parsedInput.error),
|
||||
},
|
||||
};
|
||||
return method.rawResult ? errorResult as Output : errorResult;
|
||||
}
|
||||
|
||||
try {
|
||||
const output = await method.handler(parsedInput.data);
|
||||
const parsedOutput = method.output.safeParse(output);
|
||||
if (!parsedOutput.success) {
|
||||
return {
|
||||
const errorResult: IpcResult<Output> = {
|
||||
ok: false,
|
||||
error: {
|
||||
code: "INTERNAL_ERROR",
|
||||
message: "Invalid IPC response.",
|
||||
},
|
||||
};
|
||||
return method.rawResult ? errorResult as Output : errorResult;
|
||||
}
|
||||
return { ok: true, data: parsedOutput.data };
|
||||
return method.rawResult ? parsedOutput.data : { ok: true, data: parsedOutput.data };
|
||||
} catch (error) {
|
||||
return {
|
||||
const errorResult: IpcResult<Output> = {
|
||||
ok: false,
|
||||
error: {
|
||||
code: "INTERNAL_ERROR",
|
||||
message: error instanceof Error ? error.message : "Unhandled IPC failure.",
|
||||
},
|
||||
};
|
||||
return method.rawResult ? errorResult as Output : errorResult;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ import { startAutoSnapshot } from "./snapshotService.js";
|
||||
import { registerFileHandlers } from "./fileHandler.js";
|
||||
import type { ClientSettings, RpcMethod, RpcResult } from "@mosaiciq/contracts/rpc";
|
||||
import { isRpcMethod, parseRpcRequest, parseRpcResponse } from "@mosaiciq/contracts/rpcSchemas";
|
||||
import { getDatabase, seedDatabase, getDataDir, updateClientSettings, getClientSettings } from "@mosaiciq/shared/db";
|
||||
import { getDatabase, bootstrapDatabase, getDataDir, updateClientSettings, getClientSettings } from "@mosaiciq/shared/db";
|
||||
import { eventEmitter } from "@mosaiciq/shared/agents";
|
||||
import { z } from "zod";
|
||||
import { registerIpcMethod } from "./ipc.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const isDev = process.env.VITE_DEV_SERVER_URL || !app.isPackaged;
|
||||
@@ -44,34 +45,49 @@ function rpcValidationDetail(error: z.ZodError): unknown {
|
||||
return error.issues.map((issue) => ({ path: issue.path.join("."), message: issue.message }));
|
||||
}
|
||||
|
||||
ipcMain.handle("rpc:call", async (_event, method: unknown, payload: unknown): Promise<RpcResult<RpcMethod>> => {
|
||||
if (!isRpcMethod(method)) {
|
||||
return { ok: false, error: { code: "VALIDATION_ERROR", message: "Unknown RPC method." } };
|
||||
}
|
||||
registerIpcMethod<unknown[], RpcResult<RpcMethod>>({
|
||||
channel: "rpc:call",
|
||||
input: z.tuple([z.unknown(), z.unknown()]),
|
||||
output: z.custom<RpcResult<RpcMethod>>((value) => {
|
||||
if (!value || typeof value !== "object" || !("ok" in value)) return false;
|
||||
const result = value as RpcResult<RpcMethod>;
|
||||
return result.ok ? "data" in result : "error" in result;
|
||||
}),
|
||||
rawArgs: true,
|
||||
rawResult: true,
|
||||
handler: async ([method, payload]): Promise<RpcResult<RpcMethod>> => {
|
||||
if (!isRpcMethod(method)) {
|
||||
return { ok: false, error: { code: "VALIDATION_ERROR", message: "Unknown RPC method." } };
|
||||
}
|
||||
|
||||
let parsedPayload;
|
||||
try {
|
||||
parsedPayload = parseRpcRequest(method, payload);
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "Invalid RPC payload.",
|
||||
let parsedPayload;
|
||||
try {
|
||||
parsedPayload = parseRpcRequest(method, payload);
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: "VALIDATION_ERROR",
|
||||
message: "Invalid RPC payload.",
|
||||
detail: error instanceof z.ZodError ? rpcValidationDetail(error) : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const result = await handleRpc(method, parsedPayload);
|
||||
if (!result.ok) return result;
|
||||
|
||||
try {
|
||||
parseRpcResponse(method, result.data);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("[RPC] Invalid response:", {
|
||||
method,
|
||||
detail: error instanceof z.ZodError ? rpcValidationDetail(error) : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const result = await handleRpc(method, parsedPayload);
|
||||
if (!result.ok) return result;
|
||||
|
||||
try {
|
||||
parseRpcResponse(method, result.data);
|
||||
return result;
|
||||
} catch {
|
||||
return { ok: false, error: { code: "INTERNAL_ERROR", message: "Invalid RPC response." } };
|
||||
}
|
||||
});
|
||||
return { ok: false, error: { code: "INTERNAL_ERROR", message: "Invalid RPC response." } };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
@@ -91,11 +107,7 @@ app.whenReady().then(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Seed demo data if empty (check if any companies exist)
|
||||
const companyCount = db.prepare("SELECT COUNT(*) as count FROM companies").get() as { count: number };
|
||||
if (companyCount.count === 0) {
|
||||
seedDatabase(db);
|
||||
}
|
||||
bootstrapDatabase(db);
|
||||
|
||||
const win = await createWindow();
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ const shared = {
|
||||
outDir: "dist-electron",
|
||||
sourcemap: true,
|
||||
outExtensions: () => ({ js: ".cjs" }),
|
||||
deps: {
|
||||
neverBundle: ["electron"],
|
||||
},
|
||||
};
|
||||
|
||||
export default defineConfig([
|
||||
@@ -12,7 +15,10 @@ export default defineConfig([
|
||||
...shared,
|
||||
entry: ["src/main.ts"],
|
||||
clean: true,
|
||||
noExternal: (id) => id.startsWith("@mosaiciq/"),
|
||||
deps: {
|
||||
...shared.deps,
|
||||
alwaysBundle: (id: string) => id.startsWith("@mosaiciq/"),
|
||||
},
|
||||
},
|
||||
{
|
||||
...shared,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Alert, Catalyst, EarningsSchedule, ExportRecord, Filing, Holding, Risk, Screen } from "@mosaiciq/contracts/rpc";
|
||||
import type { Screen } from "@mosaiciq/contracts/rpc";
|
||||
|
||||
export const screens: Screen[] = ["home", "workspace", "model", "memo", "agents"];
|
||||
|
||||
@@ -26,55 +26,6 @@ export const agentCatalog: Array<[string, string, string, string]> = [
|
||||
["qa", "Model QA Agent", "Formula auditing, balance sheet checks, sanity tests", "cross-cutting"]
|
||||
];
|
||||
|
||||
export const extraHoldings: Holding[] = [
|
||||
{ ticker: "BJ", name: "BJ's Wholesale Club", price: 84.2, changePct: 0.4, weight: 8 },
|
||||
{ ticker: "KR", name: "Kroger Co", price: 62.1, changePct: -0.6, weight: 5 },
|
||||
{ ticker: "DG", name: "Dollar General", price: 78.5, changePct: 1.5, weight: 4 }
|
||||
];
|
||||
|
||||
export const demoCatalysts: Catalyst[] = [
|
||||
{ id: "cat-1", date: "2026-06-05", event: "Q3 FY25 Earnings", impact: "high", thesisRelevance: "supports", source: "[4]" },
|
||||
{ id: "cat-2", date: "2026-07-15", event: "Executive member update", impact: "medium", thesisRelevance: "supports", source: "[2]" },
|
||||
{ id: "cat-3", date: "2026-08-01", event: "Annual membership fee review", impact: "high", thesisRelevance: "neutral", source: "[1]" },
|
||||
{ id: "cat-4", date: "2026-09-10", event: "New warehouse openings (Q4)", impact: "low", thesisRelevance: "supports", source: "[1]" }
|
||||
];
|
||||
|
||||
export const demoAlerts: Alert[] = [
|
||||
{ id: "alert-1", companyId: "cost", timestamp: "2026-05-12T08:30:00Z", type: "earnings_surprise", description: "Q2 FY25 earnings beat (+7.5% comp)", thesisImpact: "positive", status: "new", targetSection: "thesis" },
|
||||
{ id: "alert-2", companyId: "cost", timestamp: "2026-05-12T06:00:00Z", type: "filing", description: "New 8-K: Executive compensation update", thesisImpact: "neutral", status: "new" },
|
||||
{ id: "alert-3", companyId: "wmt", timestamp: "2026-05-11T14:00:00Z", type: "peer_event", description: "WMT announces price investment in grocery", thesisImpact: "negative", status: "reviewed" },
|
||||
{ id: "alert-4", companyId: "cost", timestamp: "2026-05-11T10:00:00Z", type: "price_move", description: "COST +2.1% on heavy volume", thesisImpact: "positive", status: "reviewed" }
|
||||
];
|
||||
|
||||
export const demoRisks: Risk[] = [
|
||||
{ id: "risk-1", companyId: "cost", risk: "Amazon enters warehouse club segment", category: "competitive", severity: "high", likelihood: "low", mitigation: "Costco's 93% renewal rate creates switching costs", status: "open" },
|
||||
{ id: "risk-2", companyId: "cost", risk: "Wage inflation compresses operating margin", category: "financial", severity: "medium", likelihood: "high", mitigation: "Automation and productivity offset 40-60% of wage pressure", status: "open" },
|
||||
{ id: "risk-3", companyId: "cost", risk: "Membership fee increase delayed beyond FY26", category: "financial", severity: "medium", likelihood: "medium", mitigation: "Fee income growth from executive tier conversion", status: "mitigated" },
|
||||
{ id: "risk-4", companyId: "cost", risk: "Regulatory pressure on merchandise sourcing", category: "regulatory", severity: "low", likelihood: "low", mitigation: "Diversified supply chain across 14 countries", status: "accepted" },
|
||||
{ id: "risk-5", companyId: "cost", risk: "E-commerce disruption of warehouse model", category: "competitive", severity: "medium", likelihood: "medium", mitigation: "Costco.com growth and Instacart partnership", status: "open" },
|
||||
{ id: "risk-6", companyId: "cost", risk: "Valuation compression on growth deceleration", category: "financial", severity: "high", likelihood: "medium", mitigation: "High-single-digit earnings growth supports premium", status: "open" }
|
||||
];
|
||||
|
||||
export const demoEarnings: EarningsSchedule[] = [
|
||||
{ id: "earn-1", companyId: "cost", quarter: "Q3 FY25", expectedDate: "2026-06-05", timing: "bmo" },
|
||||
{ id: "earn-2", companyId: "cost", quarter: "Q4 FY25", expectedDate: "2026-09-25", timing: "bmo" },
|
||||
{ id: "earn-3", companyId: "cost", quarter: "Q2 FY25", expectedDate: "2026-03-06", timing: "bmo", actualRevenue: "$62.5B", expectedRevenue: "$61.2B", actualEps: "$4.28", expectedEps: "$4.05" },
|
||||
{ id: "earn-4", companyId: "cost", quarter: "Q1 FY25", expectedDate: "2025-12-12", timing: "bmo", actualRevenue: "$60.2B", expectedRevenue: "$59.8B", actualEps: "$4.04", expectedEps: "$3.98" }
|
||||
];
|
||||
|
||||
export const demoFilings: Filing[] = [
|
||||
{ id: "filing-1", companyId: "cost", formType: "10-K", filedDate: "2024-10-10", title: "Annual Report FY2024", keyChanges: "Updated segment reporting, new warehouse commitments ($2.1B)", reviewed: true },
|
||||
{ id: "filing-2", companyId: "cost", formType: "10-Q", filedDate: "2026-03-10", title: "Quarterly Report Q2 FY25", keyChanges: "Membership fee income growth, margin expansion", reviewed: true },
|
||||
{ id: "filing-3", companyId: "cost", formType: "8-K", filedDate: "2026-05-12", title: "Executive Compensation Update", keyChanges: "New CEO compensation structure", reviewed: false }
|
||||
];
|
||||
|
||||
export const demoExports: ExportRecord[] = [
|
||||
{ id: "export-1", type: "excel", title: "COST Model — Revenue Build FY25-FY27", companyId: "cost", format: "Excel", fileSize: "2.4 MB", status: "complete", createdAt: "2026-05-10T14:30:00Z" },
|
||||
{ id: "export-2", type: "pdf", title: "COST Investment Memo — Draft", companyId: "cost", format: "PDF", fileSize: "1.8 MB", status: "complete", createdAt: "2026-05-09T16:00:00Z" },
|
||||
{ id: "export-3", type: "ppt", title: "COST IC Presentation", companyId: "cost", format: "PowerPoint", fileSize: "4.2 MB", status: "processing", createdAt: "2026-05-12T09:00:00Z" },
|
||||
{ id: "export-4", type: "pdf", title: "Peer Comparison Report", companyId: "cost", format: "PDF", fileSize: "980 KB", status: "complete", createdAt: "2026-05-08T11:00:00Z" }
|
||||
];
|
||||
|
||||
export const keyboardShortcuts: Array<[string, string, string]> = [
|
||||
["⌘K", "Open agent chat / command bar", "global"],
|
||||
["⌘F", "Focus search bar", "global"],
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import type { RpcClient, RpcMethod, RpcRequestMap } from "../../../packages/contracts/src/rpc";
|
||||
import { parseRpcResponse } from "../../../packages/contracts/src/rpcSchemas";
|
||||
import { handleMockRpc } from "../../../packages/shared/src/mockRpc";
|
||||
|
||||
export const rpc: RpcClient = {
|
||||
async call<T extends RpcMethod>(method: T, payload: RpcRequestMap[T]) {
|
||||
const useMockRpc = !window.mosaic;
|
||||
if (useMockRpc) {
|
||||
const result = await handleMockRpc(method, payload);
|
||||
if (process.env.NODE_ENV !== "production" && result.ok) {
|
||||
parseRpcResponse(method, result.data);
|
||||
}
|
||||
return result;
|
||||
if (!window.mosaic) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: "INTERNAL_ERROR",
|
||||
message: "Desktop backend unavailable. Run MosaicIQ in the Electron app to use the local SQLite backend."
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return window.mosaic!.call(method, payload);
|
||||
return window.mosaic.call(method, payload);
|
||||
}
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
3915
apps/web/src/ui/app/AppShell.tsx
Normal file
3915
apps/web/src/ui/app/AppShell.tsx
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user