From f0f22129be76901d4d78740b6bdebfa54710a412 Mon Sep 17 00:00:00 2001
From: nyk <93952610+0xNyk@users.noreply.github.com>
Date: Tue, 3 Mar 2026 14:19:34 +0700
Subject: [PATCH] fix: healthcheck auth, secure cookie auto-detect, model
object crash (#84)
Bug 1 (#78): Dockerfile HEALTHCHECK curled authenticated /api/status,
always got 401 in production. Changed to /login which is public.
Bug 2 (#78): Login hangs on HTTP deployments because secure=true cookie
is silently rejected. Now auto-detects protocol from x-forwarded-proto
header, only sets secure when request actually came over HTTPS.
Bug 3 (#78): Agent model field from OpenClaw 2026.3.x is {primary: "name"}
object instead of string, causing React error #31. Added normalizeModel()
helper and applied it in all WebSocket/session mapping code paths.
---
Dockerfile | 2 +-
src/app/api/auth/google/route.ts | 5 ++++-
src/app/api/auth/login/route.ts | 5 ++++-
src/components/dashboard/agent-network.tsx | 2 +-
src/lib/session-cookie.ts | 6 ++++--
src/lib/sessions.ts | 2 +-
src/lib/utils.ts | 7 +++++++
src/lib/websocket.ts | 7 ++++---
8 files changed, 26 insertions(+), 10 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index a9a85a3..b5e7c55 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -29,5 +29,5 @@ EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
- CMD curl -f http://localhost:3000/api/status || exit 1
+ CMD curl -f http://localhost:3000/login || exit 1
CMD ["node", "server.js"]
diff --git a/src/app/api/auth/google/route.ts b/src/app/api/auth/google/route.ts
index c574057..a683074 100644
--- a/src/app/api/auth/google/route.ts
+++ b/src/app/api/auth/google/route.ts
@@ -92,8 +92,11 @@ export async function POST(request: Request) {
},
})
+ const isSecureRequest = request.headers.get('x-forwarded-proto') === 'https'
+ || new URL(request.url).protocol === 'https:'
+
response.cookies.set('mc-session', token, {
- ...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000) }),
+ ...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000), isSecureRequest }),
})
return response
diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts
index 2da7472..57a5274 100644
--- a/src/app/api/auth/login/route.ts
+++ b/src/app/api/auth/login/route.ts
@@ -41,8 +41,11 @@ export async function POST(request: Request) {
},
})
+ const isSecureRequest = request.headers.get('x-forwarded-proto') === 'https'
+ || new URL(request.url).protocol === 'https:'
+
response.cookies.set('mc-session', token, {
- ...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000) }),
+ ...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000), isSecureRequest }),
})
return response
diff --git a/src/components/dashboard/agent-network.tsx b/src/components/dashboard/agent-network.tsx
index 13ee8a9..7628ac7 100644
--- a/src/components/dashboard/agent-network.tsx
+++ b/src/components/dashboard/agent-network.tsx
@@ -86,7 +86,7 @@ function AgentNode({ data }: { data: any }) {
- {agent.model.split('/').pop()}
+ {(typeof agent.model === 'string' ? agent.model : '').split('/').pop() || 'unknown'}
{agent.session && (
diff --git a/src/lib/session-cookie.ts b/src/lib/session-cookie.ts
index 5363668..508e432 100644
--- a/src/lib/session-cookie.ts
+++ b/src/lib/session-cookie.ts
@@ -9,9 +9,11 @@ function envFlag(name: string): boolean | undefined {
return undefined
}
-export function getMcSessionCookieOptions(input: { maxAgeSeconds: number }): Partial {
+export function getMcSessionCookieOptions(input: { maxAgeSeconds: number; isSecureRequest?: boolean }): Partial {
const secureEnv = envFlag('MC_COOKIE_SECURE')
- const secure = secureEnv ?? process.env.NODE_ENV === 'production'
+ // Explicit env wins. Otherwise auto-detect: only set secure if request came over HTTPS.
+ // Falls back to NODE_ENV=production when no request hint is available.
+ const secure = secureEnv ?? input.isSecureRequest ?? process.env.NODE_ENV === 'production'
// Strict is safest for this app (same-site UI + API), but allow override for edge cases.
const sameSiteRaw = (process.env.MC_COOKIE_SAMESITE || 'strict').toLowerCase()
diff --git a/src/lib/sessions.ts b/src/lib/sessions.ts
index 91165ba..8983e6e 100644
--- a/src/lib/sessions.ts
+++ b/src/lib/sessions.ts
@@ -62,7 +62,7 @@ export function getAllGatewaySessions(activeWithinMs = 60 * 60 * 1000): GatewayS
updatedAt,
chatType: s.chatType || 'unknown',
channel: s.deliveryContext?.channel || s.lastChannel || s.channel || '',
- model: s.model || '',
+ model: typeof s.model === 'object' && s.model?.primary ? String(s.model.primary) : String(s.model || ''),
totalTokens: s.totalTokens || 0,
inputTokens: s.inputTokens || 0,
outputTokens: s.outputTokens || 0,
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 57a6d77..67f9498 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -59,6 +59,13 @@ export function getStatusBadgeColor(status: AgentStatus['status']): string {
}
}
+/** Normalize model field — OpenClaw 2026.3.x may send {primary: "model-name"} instead of a string */
+export function normalizeModel(model: unknown): string {
+ if (typeof model === 'string') return model
+ if (model && typeof model === 'object' && 'primary' in model) return String((model as any).primary)
+ return ''
+}
+
export function sessionToAgent(session: Session): Agent {
const getStatusFromSession = (session: Session): AgentStatus['status'] => {
if (session.age === 'just now' || session.age.includes('m ago')) return 'active'
diff --git a/src/lib/websocket.ts b/src/lib/websocket.ts
index 53b86d1..7acb8fa 100644
--- a/src/lib/websocket.ts
+++ b/src/lib/websocket.ts
@@ -2,6 +2,7 @@
import { useCallback, useRef, useEffect } from 'react'
import { useMissionControl } from '@/store'
+import { normalizeModel } from '@/lib/utils'
// Gateway protocol version (v3 required by OpenClaw 2026.x)
const PROTOCOL_VERSION = 3
@@ -172,7 +173,7 @@ export function useWebSocket() {
key: session.key || '',
kind: session.kind || 'unknown',
age: session.age || '',
- model: session.model || '',
+ model: normalizeModel(session.model),
tokens: session.tokens || '',
flags: session.flags || [],
active: session.active || false,
@@ -219,7 +220,7 @@ export function useWebSocket() {
// Handle various gateway events
if (message.data?.type === 'token_usage') {
addTokenUsage({
- model: message.data.model,
+ model: normalizeModel(message.data.model),
sessionId: message.data.sessionId,
date: new Date().toISOString(),
inputTokens: message.data.inputTokens || 0,
@@ -291,7 +292,7 @@ export function useWebSocket() {
key: session.key || '',
kind: session.kind || 'unknown',
age: formatAge(session.updatedAt),
- model: session.model || '',
+ model: normalizeModel(session.model),
tokens: `${session.totalTokens || 0}/${session.contextTokens || 35000}`,
flags: [],
active: isActive(session.updatedAt),