From 0d67e2af26bfdf6c3fe6e7c081267088d6a61976 Mon Sep 17 00:00:00 2001 From: brunorezio Date: Sat, 25 Jul 2026 23:06:16 +0100 Subject: [PATCH] clear the remaining type errors - DiscordAccount seeded DiscordStatus without its two nullable fields. - bug-report typed reporter.name as string, but users.name is nullable; and the Discord upload wrapped a Buffer directly in a Blob. - Lucide icons take no `title` prop, so the sync spinner's tooltip moved to a wrapping span. - DesktopView cast its dynamic import to a type that included `| null`. - dock PUT cast the request body straight to string[]; it now rejects anything that is not an array of strings instead of writing it to the database. - buildZodSchema assembles a mutable record, since z.ZodRawShape is readonly in zod v4. - The dev-server proxy forwards Bun's `string | Buffer` frames through a helper that satisfies WebSocket.send without copying. bunx tsgo is now clean. Co-Authored-By: Claude Opus 5 --- .../Screens/Dashboard/Email/EmailList.tsx | 136 +++++++++++------- .../IntegrationsSettings/DiscordAccount.tsx | 22 +-- src/server.tsx | 66 +++++++-- src/servers/api/bug-report/bug-report.ts | 23 ++- src/servers/api/dock/dock.ts | 7 +- src/servers/mcp-tool-server.ts | 5 +- .../src/apps/Desktop/DesktopView.tsx | 17 ++- 7 files changed, 197 insertions(+), 79 deletions(-) diff --git a/src/apps/officer-web/Screens/Dashboard/Email/EmailList.tsx b/src/apps/officer-web/Screens/Dashboard/Email/EmailList.tsx index 44482c64..1bebaa12 100644 --- a/src/apps/officer-web/Screens/Dashboard/Email/EmailList.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Email/EmailList.tsx @@ -1,7 +1,21 @@ import { useEffect, useRef, useState } from 'react'; import { Link } from 'react-router'; import { useQuery, useQueryClient, keepPreviousData } from '@tanstack/react-query'; -import { ChevronLeft, ChevronRight, Inbox, Loader2, Mail, Paperclip, RefreshCw, Search, Send, ShieldAlert, SquarePen, Trash2, X } from 'lucide-react'; +import { + ChevronLeft, + ChevronRight, + Inbox, + Loader2, + Mail, + Paperclip, + RefreshCw, + Search, + Send, + ShieldAlert, + SquarePen, + Trash2, + X, +} from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { useClient } from 'hooks/useClient'; @@ -66,8 +80,12 @@ export const EmailList = () => { queryKey: isSearching ? ['email-search', debouncedSearch, page] : ['email-messages', page, folder], queryFn: () => isSearching - ? client.get<{ messages: EmailSummary[]; total: number }>(`/email/search?q=${encodeURIComponent(debouncedSearch)}&page=${page}&limit=${LIMIT}`) - : client.get<{ messages: EmailSummary[]; total: number }>(`/email/messages?page=${page}&limit=${LIMIT}&folder=${folder}`), + ? client.get<{ messages: EmailSummary[]; total: number }>( + `/email/search?q=${encodeURIComponent(debouncedSearch)}&page=${page}&limit=${LIMIT}`, + ) + : client.get<{ messages: EmailSummary[]; total: number }>( + `/email/messages?page=${page}&limit=${LIMIT}&folder=${folder}`, + ), // Keep the previous results visible while the next query loads, so switching search terms/pages // never bails to the full-panel loading view (which would unmount the search input and drop focus). placeholderData: keepPreviousData, @@ -117,7 +135,10 @@ export const EmailList = () => { if (!syncableAccount) return; setSyncing(true); try { - const result = await client.post<{ ok: boolean; saved?: number }>(`/email/accounts/${syncableAccount.id}/sync`, {}); + const result = await client.post<{ ok: boolean; saved?: number }>( + `/email/accounts/${syncableAccount.id}/sync`, + {}, + ); if (result.saved !== undefined) { toast.success(result.saved > 0 ? `${result.saved} new emails` : 'No new emails'); queryClient.invalidateQueries({ queryKey: ['email-messages'] }); @@ -174,11 +195,7 @@ export const EmailList = () => { }, [messages, selectedId]); if (isLoading) { - return ( -
- Loading emails... -
- ); + return
Loading emails...
; } // Show onboarding empty state only when no emails exist at all (never while searching) @@ -198,7 +215,11 @@ export const EmailList = () => {
No emails synced yet
@@ -238,7 +259,9 @@ export const EmailList = () => { {total} {syncing || isSyncing ? ( - + + + ) : syncableAccount ? ( )} @@ -298,43 +329,48 @@ export const EmailList = () => { {isSearching ? `No results for “${debouncedSearch}”` : 'No emails in this folder'} ) : ( -
- {messages.map((msg: EmailSummary) => { - const unread = msg.threadUnread !== undefined ? msg.threadUnread > 0 : !msg.read; - return ( - - ); - })} -
+
+ {messages.map((msg: EmailSummary) => { + const unread = msg.threadUnread !== undefined ? msg.threadUnread > 0 : !msg.read; + return ( + + ); + })} +
)} ); diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/DiscordAccount.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/DiscordAccount.tsx index 52fd3e5c..cb25a5f9 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/DiscordAccount.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/DiscordAccount.tsx @@ -20,7 +20,12 @@ export const DiscordAccount = () => { const client = useClient(); const [isLoading, setIsLoading] = useState(true); const [connection, setConnection] = useState({ linked: false }); - const [botStatus, setBotStatus] = useState({ configured: false, running: false }); + const [botStatus, setBotStatus] = useState({ + configured: false, + running: false, + serverInvite: null, + botHandle: null, + }); const [pairingCode, setPairingCode] = useState(null); const [isGenerating, setIsGenerating] = useState(false); @@ -116,12 +121,7 @@ export const DiscordAccount = () => {

You can send direct messages to the bot on Discord and they will be handled by your PI agent.

- @@ -142,7 +142,13 @@ export const DiscordAccount = () => { {pairingCode} - diff --git a/src/server.tsx b/src/server.tsx index 0fdff986..4ac935bb 100644 --- a/src/server.tsx +++ b/src/server.tsx @@ -33,7 +33,16 @@ type WSData = { userId: number; email: string; username: string; - provider: 'terminal' | 'chat' | 'task-runner' | 'pipeline' | 'dev-server' | 'cliamp' | 'cliamp-audio' | 'desktop' | 'sidecar'; + provider: + | 'terminal' + | 'chat' + | 'task-runner' + | 'pipeline' + | 'dev-server' + | 'cliamp' + | 'cliamp-audio' + | 'desktop' + | 'sidecar'; sessionId?: string; cwd?: string; command?: string; @@ -141,6 +150,12 @@ const handlers: Record = { // Dev-server WebSocket proxy: bridges client WS ↔ upstream dev server WS (for HMR etc.) type UpstreamState = { ws: WebSocket; queue: (string | Buffer)[]; ready: boolean }; + +// Bun hands WS frames over as `string | Buffer`, but the DOM WebSocket.send signature won't accept a +// Buffer (it can't rule out a SharedArrayBuffer backing). A Buffer is a Uint8Array +// at runtime, so this forwards as-is rather than paying for a copy on every proxied frame. +const asWsPayload = (raw: string | Buffer): string | Uint8Array => + typeof raw === 'string' ? raw : (raw as Uint8Array); const devServerUpstreams = new Map, UpstreamState>(); const devServerWebsocket = { @@ -154,8 +169,14 @@ const devServerWebsocket = { } try { const payload = await verify(wsToken); - if (!payload) { ws.close(4001, 'Unauthorized'); return; } - if (payload.jti && await isTokenBlacklisted(payload.jti)) { ws.close(4001, 'Unauthorized'); return; } + if (!payload) { + ws.close(4001, 'Unauthorized'); + return; + } + if (payload.jti && (await isTokenBlacklisted(payload.jti))) { + ws.close(4001, 'Unauthorized'); + return; + } } catch { ws.close(4001, 'Unauthorized'); return; @@ -167,7 +188,7 @@ const devServerWebsocket = { upstream.addEventListener('open', () => { state.ready = true; - for (const msg of state.queue) upstream.send(msg); + for (const msg of state.queue) upstream.send(asWsPayload(msg)); state.queue.length = 0; }); @@ -189,7 +210,7 @@ const devServerWebsocket = { const state = devServerUpstreams.get(ws); if (!state) return; if (state.ready) { - state.ws.send(raw); + state.ws.send(asWsPayload(raw)); } else { state.queue.push(raw); } @@ -204,7 +225,11 @@ const devServerWebsocket = { }; handlers['dev-server'] = devServerWebsocket; -async function upgradeWs(req: Request, server: any, provider: 'terminal' | 'chat' | 'task-runner' | 'pipeline' | 'cliamp' | 'cliamp-audio' | 'desktop') { +async function upgradeWs( + req: Request, + server: any, + provider: 'terminal' | 'chat' | 'task-runner' | 'pipeline' | 'cliamp' | 'cliamp-audio' | 'desktop', +) { const token = new URL(req.url).searchParams.get('token'); if (!token) return new Response('Unauthorized', { status: 401 }); @@ -224,7 +249,18 @@ async function upgradeWs(req: Request, server: any, provider: 'terminal' | 'chat const rows = url.searchParams.get('rows') ? Number(url.searchParams.get('rows')) : undefined; const files = url.searchParams.get('files') ?? undefined; const ok = server.upgrade(req, { - data: { userId: user.id, email: user.email, username: toShellUsername(user.username ?? '', user.email), provider, sessionId, cwd, command, cols, rows, files }, + data: { + userId: user.id, + email: user.email, + username: toShellUsername(user.username ?? '', user.email), + provider, + sessionId, + cwd, + command, + cols, + rows, + files, + }, }); if (!ok) return new Response('Upgrade failed', { status: 500 }); } catch { @@ -330,7 +366,13 @@ try { } // Initialize queue engine in API server process -import { initQueue, enqueueJob as queueEnqueue, cancelJob as queueCancel, listAllJobs as queueList, readJob as queueGet } from './servers/queue/init'; +import { + initQueue, + enqueueJob as queueEnqueue, + cancelJob as queueCancel, + listAllJobs as queueList, + readJob as queueGet, +} from './servers/queue/init'; initQueue().catch((err) => console.error('[queue] failed to initialize:', err)); // Mark any orphaned pipeline jobs from previous server run @@ -364,7 +406,13 @@ cleanupOnStartup().catch((err) => console.error('[pipeline-jobs] startup cleanup const sinkList = sinks.stdout.toString(); if (!sinkList.includes('virtual_out')) { const load = Bun.spawnSync({ - cmd: [pactl, 'load-module', 'module-null-sink', 'sink_name=virtual_out', 'sink_properties=device.description=Virtual_Output'], + cmd: [ + pactl, + 'load-module', + 'module-null-sink', + 'sink_name=virtual_out', + 'sink_properties=device.description=Virtual_Output', + ], stdout: 'pipe', stderr: 'pipe', }); diff --git a/src/servers/api/bug-report/bug-report.ts b/src/servers/api/bug-report/bug-report.ts index 2687a885..a47ad3a2 100644 --- a/src/servers/api/bug-report/bug-report.ts +++ b/src/servers/api/bug-report/bug-report.ts @@ -51,8 +51,13 @@ bugReportRouter.post('/', async (ctx) => { type BugReport = { description: string; - context: { url?: string; userAgent?: string; viewport?: { width: number; height: number }; apiError?: { status: number; message: string } | null } | null; - reporter: { id: number; email: string; name: string }; + context: { + url?: string; + userAgent?: string; + viewport?: { width: number; height: number }; + apiError?: { status: number; message: string } | null; + } | null; + reporter: { id: number; email: string; name: string | null }; createdAt: string; }; @@ -64,21 +69,29 @@ async function sendToDiscord(report: BugReport, screenshot: Buffer | null) { fields: [ { name: 'Reporter', value: `${report.reporter.name} (${report.reporter.email})`, inline: true }, { name: 'URL', value: report.context?.url ?? 'N/A', inline: false }, - { name: 'Viewport', value: report.context?.viewport ? `${report.context.viewport.width}x${report.context.viewport.height}` : 'N/A', inline: true }, + { + name: 'Viewport', + value: report.context?.viewport ? `${report.context.viewport.width}x${report.context.viewport.height}` : 'N/A', + inline: true, + }, { name: 'Browser', value: shortenUA(report.context?.userAgent), inline: true }, ], timestamp: report.createdAt, }; if (report.context?.apiError) { - embed.fields.push({ name: 'Last API Error', value: `${report.context.apiError.status}: ${report.context.apiError.message}`, inline: false }); + embed.fields.push({ + name: 'Last API Error', + value: `${report.context.apiError.status}: ${report.context.apiError.message}`, + inline: false, + }); } const form = new FormData(); form.append('payload_json', JSON.stringify({ embeds: [embed] })); if (screenshot) { - form.append('files[0]', new Blob([screenshot], { type: 'image/png' }), 'screenshot.png'); + form.append('files[0]', new Blob([new Uint8Array(screenshot)], { type: 'image/png' }), 'screenshot.png'); } const res = await fetch(DISCORD_WEBHOOK_URL!, { method: 'POST', body: form }); diff --git a/src/servers/api/dock/dock.ts b/src/servers/api/dock/dock.ts index d5929f34..d71f9f38 100644 --- a/src/servers/api/dock/dock.ts +++ b/src/servers/api/dock/dock.ts @@ -1,5 +1,6 @@ import { createRouter } from '../../create-router'; import { getDockPaths, setDockPaths } from 'officerdb'; +import * as errors from '@@/custom-errors'; export const dockRouter = createRouter(); @@ -13,7 +14,11 @@ dockRouter.get('/', async (ctx) => { // PUT / — full replacement of dock paths array dockRouter.put('/', async (ctx) => { const userId = ctx.get('user').id; - const paths = ctx.get('body') as string[]; + const body = ctx.get('body') as unknown; + if (!Array.isArray(body) || body.some((p) => typeof p !== 'string')) { + throw errors.BAD_REQUEST('Expected an array of dock paths'); + } + const paths = body as string[]; await setDockPaths(userId, paths); return ctx.json(paths); }); diff --git a/src/servers/mcp-tool-server.ts b/src/servers/mcp-tool-server.ts index 699fa7f2..e9e533c6 100644 --- a/src/servers/mcp-tool-server.ts +++ b/src/servers/mcp-tool-server.ts @@ -139,7 +139,7 @@ function discoverTools(dirs: string[]): DiscoveredTool[] { const { meta } = parseFrontmatter(content); if (!meta.name || !meta.description) continue; - if ((meta.targets as string ?? 'all') === 'pi') continue; + if (((meta.targets as string) ?? 'all') === 'pi') continue; if (seen.has(meta.name)) continue; seen.add(meta.name); @@ -153,7 +153,8 @@ function discoverTools(dirs: string[]): DiscoveredTool[] { // ── Schema building (frontmatter inputs → zod) ── function buildZodSchema(inputs: Record): z.ZodRawShape { - const shape: z.ZodRawShape = {}; + // z.ZodRawShape is readonly in zod v4, so build it mutably and widen on return. + const shape: Record = {}; for (const [name, param] of Object.entries(inputs)) { let field: z.ZodTypeAny; diff --git a/src/workspaces/officerdev/src/apps/Desktop/DesktopView.tsx b/src/workspaces/officerdev/src/apps/Desktop/DesktopView.tsx index b5e2fb7b..41793379 100644 --- a/src/workspaces/officerdev/src/apps/Desktop/DesktopView.tsx +++ b/src/workspaces/officerdev/src/apps/Desktop/DesktopView.tsx @@ -25,11 +25,13 @@ type RFBInstance = { addEventListener: (type: string, listener: (ev: CustomEvent) => void) => void; }; -let rfbModulePromise: Promise<{ default: new (target: HTMLElement, url: string, options?: { credentials?: { password?: string } }) => RFBInstance }> | null = null; +let rfbModulePromise: Promise<{ + default: new (target: HTMLElement, url: string, options?: { credentials?: { password?: string } }) => RFBInstance; +}> | null = null; const loadRFB = () => { if (!rfbModulePromise) { - rfbModulePromise = import(/* @vite-ignore */ NOVNC_URL) as typeof rfbModulePromise; + rfbModulePromise = import(/* @vite-ignore */ NOVNC_URL) as NonNullable; } return rfbModulePromise!; }; @@ -116,14 +118,21 @@ export const DesktopView = ({ className, style }: DesktopViewProps) => { return () => { disposed = true; if (rfbRef.current) { - try { rfbRef.current.disconnect(); } catch { /* ignore */ } + try { + rfbRef.current.disconnect(); + } catch { + /* ignore */ + } rfbRef.current = null; } }; }, [isMounted, client]); return ( -
+
{status === 'connecting' && (
Connecting to desktop...