Compare commits

..

10 Commits

Author SHA1 Message Date
27120ac3f2 fix(deploy): use consistent network alias for openbrain-brain-internal
All checks were successful
ci/woodpecker/push/ci Infra-only: compose YAML fix, no app code
Service definitions were using 'openbrain_brain-internal' (underscore) but the
networks block defines the alias as 'openbrain-brain-internal' (hyphen), with
name: openbrain_brain-internal pointing to the actual Docker network.

This caused 'undefined network' errors on every Portainer deploy for
orchestrator and synapse services.

Fixed: all service network references now use 'openbrain-brain-internal'.
2026-03-08 11:34:38 -05:00
ad9921107c fix(deploy): add DATABASE_URL and openbrain network to orchestrator + synapse (#745)
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-03-08 15:51:22 +00:00
3c288f9849 fix(web): add ReactQueryProvider to root layout for Mission Control (#744)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-03-08 15:50:40 +00:00
51d6302401 Merge pull request 'style(web): fix prettier formatting in AuditLogDrawer' (#743) from fix/audit-drawer-format into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
2026-03-08 01:56:03 +00:00
cf490510bf style(web): fix prettier formatting in AuditLogDrawer
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
2026-03-07 19:54:08 -06:00
3d91334df7 Merge pull request 'fix(mission-control): increase rate limit for events/recent, add error handling' (#742) from fix/mission-control-ratelimit into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
2026-03-08 00:44:31 +00:00
e80b624ca6 fix(mission-control): increase rate limit for events/recent, add error handling
Some checks failed
ci/woodpecker/push/ci Pipeline failed
2026-03-07 18:42:50 -06:00
65536fcb75 Merge pull request 'fix(orchestrator): add missing module import for OrchestratorApiKeyGuard' (#741) from fix/orchestrator-guard-import into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
2026-03-07 23:36:55 +00:00
53915dc621 fix(orchestrator): add missing module import for OrchestratorApiKeyGuard
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
2026-03-07 17:35:57 -06:00
398ee06920 Merge pull request 'chore: release v0.0.23 — Mission Control Dashboard' (#740) from chore/ms23-v0.0.23-release into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
2026-03-07 23:01:19 +00:00
6 changed files with 75 additions and 11 deletions

View File

@@ -146,7 +146,7 @@ export class AgentsController {
* Return recent orchestrator events for non-streaming consumers. * Return recent orchestrator events for non-streaming consumers.
*/ */
@Get("events/recent") @Get("events/recent")
@Throttle({ status: { limit: 200, ttl: 60000 } }) @Throttle({ default: { limit: 1000, ttl: 60000 } })
getRecentEvents(@Query("limit") limit?: string): { getRecentEvents(@Query("limit") limit?: string): {
events: ReturnType<AgentEventsService["getRecentEvents"]>; events: ReturnType<AgentEventsService["getRecentEvents"]>;
} { } {

View File

@@ -4,6 +4,6 @@ import { AuthGuard } from "./guards/auth.guard";
@Module({ @Module({
providers: [OrchestratorApiKeyGuard, AuthGuard], providers: [OrchestratorApiKeyGuard, AuthGuard],
exports: [AuthGuard], exports: [OrchestratorApiKeyGuard, AuthGuard],
}) })
export class AuthModule {} export class AuthModule {}

View File

@@ -4,6 +4,7 @@ import { Outfit, Fira_Code } from "next/font/google";
import { AuthProvider } from "@/lib/auth/auth-context"; import { AuthProvider } from "@/lib/auth/auth-context";
import { ErrorBoundary } from "@/components/error-boundary"; import { ErrorBoundary } from "@/components/error-boundary";
import { ThemeProvider } from "@/providers/ThemeProvider"; import { ThemeProvider } from "@/providers/ThemeProvider";
import { ReactQueryProvider } from "@/providers/ReactQueryProvider";
import "./globals.css"; import "./globals.css";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -56,9 +57,11 @@ export default function RootLayout({ children }: { children: ReactNode }): React
</head> </head>
<body> <body>
<ThemeProvider> <ThemeProvider>
<ReactQueryProvider>
<ErrorBoundary> <ErrorBoundary>
<AuthProvider>{children}</AuthProvider> <AuthProvider>{children}</AuthProvider>
</ErrorBoundary> </ErrorBoundary>
</ReactQueryProvider>
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>

View File

@@ -44,6 +44,25 @@ interface AuditLogResponse {
total: number; total: number;
page: number; page: number;
pages: number; pages: number;
notice?: string;
}
function createEmptyAuditLogResponse(page: number, notice?: string): AuditLogResponse {
return {
items: [],
total: 0,
page,
pages: 0,
...(notice !== undefined ? { notice } : {}),
};
}
function isRateLimitError(error: unknown): boolean {
if (!(error instanceof Error)) {
return false;
}
return /429|rate limit|too many requests/i.test(error.message);
} }
function isRecord(value: unknown): value is Record<string, unknown> { function isRecord(value: unknown): value is Record<string, unknown> {
@@ -138,7 +157,15 @@ async function fetchAuditLog(
params.set("sessionId", normalizedSessionId); params.set("sessionId", normalizedSessionId);
} }
return apiGet<AuditLogResponse>(`/api/mission-control/audit-log?${params.toString()}`); try {
return await apiGet<AuditLogResponse>(`/api/mission-control/audit-log?${params.toString()}`);
} catch (error) {
if (isRateLimitError(error)) {
return createEmptyAuditLogResponse(page, "Rate limited - retrying...");
}
return createEmptyAuditLogResponse(page);
}
} }
export function AuditLogDrawer({ sessionId, trigger }: AuditLogDrawerProps): React.JSX.Element { export function AuditLogDrawer({ sessionId, trigger }: AuditLogDrawerProps): React.JSX.Element {
@@ -180,11 +207,10 @@ export function AuditLogDrawer({ sessionId, trigger }: AuditLogDrawerProps): Rea
const totalItems = auditLogQuery.data?.total ?? 0; const totalItems = auditLogQuery.data?.total ?? 0;
const totalPages = auditLogQuery.data?.pages ?? 0; const totalPages = auditLogQuery.data?.pages ?? 0;
const items = auditLogQuery.data?.items ?? []; const items = auditLogQuery.data?.items ?? [];
const notice = auditLogQuery.data?.notice;
const canGoPrevious = page > 1; const canGoPrevious = page > 1;
const canGoNext = totalPages > 0 && page < totalPages; const canGoNext = totalPages > 0 && page < totalPages;
const errorMessage =
auditLogQuery.error instanceof Error ? auditLogQuery.error.message : "Failed to load audit log";
return ( return (
<Sheet open={open} onOpenChange={setOpen}> <Sheet open={open} onOpenChange={setOpen}>
@@ -237,10 +263,13 @@ export function AuditLogDrawer({ sessionId, trigger }: AuditLogDrawerProps): Rea
Loading audit log... Loading audit log...
</td> </td>
</tr> </tr>
) : auditLogQuery.error ? ( ) : notice ? (
<tr> <tr>
<td colSpan={5} className="px-3 py-6 text-center text-sm text-red-500"> <td
{errorMessage} colSpan={5}
className="px-3 py-6 text-center text-sm text-muted-foreground"
>
{notice}
</td> </td>
</tr> </tr>
) : items.length === 0 ? ( ) : items.length === 0 ? (

View File

@@ -0,0 +1,28 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState, type ReactNode } from "react";
interface ReactQueryProviderProps {
children: ReactNode;
}
export function ReactQueryProvider({ children }: ReactQueryProviderProps): React.JSX.Element {
// Create a stable QueryClient per component mount (one per app session)
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// Don't refetch on window focus in a dashboard context
refetchOnWindowFocus: false,
// Stale time of 30s — short enough for live data, avoids hammering
staleTime: 30_000,
retry: 1,
},
},
})
);
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}

View File

@@ -316,6 +316,8 @@ services:
SANDBOX_ENABLED: "true" SANDBOX_ENABLED: "true"
# API key for authenticating requests from the web proxy # API key for authenticating requests from the web proxy
ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY} ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY}
# Prisma database connection (uses the shared openbrain postgres)
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@openbrain_brain-db:5432/${POSTGRES_DB:-mosaic}
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
- orchestrator_workspace:/workspace - orchestrator_workspace:/workspace
@@ -331,6 +333,7 @@ services:
start_period: 40s start_period: 40s
networks: networks:
- internal - internal
- openbrain-brain-internal
cap_drop: cap_drop:
- ALL - ALL
cap_add: cap_add:
@@ -403,6 +406,7 @@ services:
networks: networks:
- internal - internal
- traefik-public - traefik-public
- openbrain-brain-internal
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure