diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 000000000..b1b897773
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,48 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - "v*"
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+ cache-dependency-path: web/package-lock.json
+
+ - name: Install frontend dependencies
+ run: cd web && npm ci
+
+ - name: Build frontend
+ run: cd web && npm run build
+
+ - name: Copy frontend assets to proxy embed directory
+ run: |
+ rm -rf proxy/frontend/dist
+ cp -r web/build/client proxy/frontend/dist
+
+ - name: Run GoReleaser with goreleaser-cross
+ run: |
+ docker run \
+ --rm \
+ -e CGO_ENABLED=1 \
+ -e GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -v ${{ github.workspace }}:/go/src/github.com/${{ github.repository }} \
+ -w /go/src/github.com/${{ github.repository }} \
+ ghcr.io/goreleaser/goreleaser-cross:v1.23.6 \
+ release --clean
diff --git a/.gitignore b/.gitignore
index 759a7302c..378e3557e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,10 @@ bin/
dist/
*.exe
+# Embedded frontend assets (populated by build)
+proxy/frontend/dist/*
+!proxy/frontend/dist/.gitkeep
+
# IDE and system files
.DS_Store
.vscode/
diff --git a/.goreleaser.yml b/.goreleaser.yml
new file mode 100644
index 000000000..a37439e2c
--- /dev/null
+++ b/.goreleaser.yml
@@ -0,0 +1,73 @@
+version: 2
+
+project_name: claude-code-proxy
+
+builds:
+ - id: linux-amd64
+ dir: proxy
+ main: ./cmd/proxy
+ binary: claude-code-proxy
+ goos:
+ - linux
+ goarch:
+ - amd64
+ env:
+ - CGO_ENABLED=1
+ - CC=x86_64-linux-gnu-gcc
+ - CXX=x86_64-linux-gnu-g++
+
+ - id: linux-arm64
+ dir: proxy
+ main: ./cmd/proxy
+ binary: claude-code-proxy
+ goos:
+ - linux
+ goarch:
+ - arm64
+ env:
+ - CGO_ENABLED=1
+ - CC=aarch64-linux-gnu-gcc
+ - CXX=aarch64-linux-gnu-g++
+
+ - id: darwin-amd64
+ dir: proxy
+ main: ./cmd/proxy
+ binary: claude-code-proxy
+ goos:
+ - darwin
+ goarch:
+ - amd64
+ env:
+ - CGO_ENABLED=1
+ - CC=o64-clang
+ - CXX=o64-clang++
+
+ - id: darwin-arm64
+ dir: proxy
+ main: ./cmd/proxy
+ binary: claude-code-proxy
+ goos:
+ - darwin
+ goarch:
+ - arm64
+ env:
+ - CGO_ENABLED=1
+ - CC=oa64-clang
+ - CXX=oa64-clang++
+
+archives:
+ - formats:
+ - tar.gz
+ name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
+
+checksum:
+ name_template: "checksums.txt"
+
+changelog:
+ sort: asc
+ filters:
+ exclude:
+ - "^docs:"
+ - "^test:"
+ - "^ci:"
+ - "^chore:"
diff --git a/Dockerfile b/Dockerfile
index 6891deda1..bff936644 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,18 @@
# Multi-stage Dockerfile for Claude Code Proxy
-# Builds both Go proxy server and Remix frontend in a single container
+# Builds a single Go binary with embedded frontend assets
-# Stage 1: Build Go Backend
+# Stage 1: Build Node.js Frontend
+FROM node:20-alpine AS node-builder
+
+WORKDIR /app/web
+
+COPY web/package*.json ./
+RUN npm ci
+
+COPY web/ ./
+RUN npm run build
+
+# Stage 2: Build Go Binary with embedded frontend
FROM golang:1.21-alpine AS go-builder
WORKDIR /app
@@ -16,28 +27,15 @@ RUN go mod download
# Copy Go source code
COPY proxy/ ./
-# Build with CGO enabled for SQLite support
-RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o /app/bin/proxy cmd/proxy/main.go
-
-# Stage 2: Build Node.js Frontend
-FROM node:20-alpine AS node-builder
-
-WORKDIR /app
-
-# Copy package files
-COPY web/package*.json ./web/
-WORKDIR /app/web
-RUN npm ci
-# Copy web source code and build
-COPY web/ ./
-RUN npm run build
+# Copy built frontend assets into embed directory
+COPY --from=node-builder /app/web/build/client ./frontend/dist
-# Clean up dev dependencies after build
-RUN npm ci --only=production && npm cache clean --force
+# Build with CGO enabled for SQLite support
+RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o /app/bin/proxy cmd/proxy/main.go
# Stage 3: Production Runtime
-FROM node:20-alpine
+FROM alpine:3.19
WORKDIR /app
@@ -48,25 +46,15 @@ RUN apk add --no-cache sqlite wget
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup
-# Copy built Go binary
+# Copy built Go binary (includes embedded frontend)
COPY --from=go-builder /app/bin/proxy ./bin/proxy
RUN chmod +x ./bin/proxy
-# Copy built Remix application
-COPY --from=node-builder /app/web/build ./web/build
-COPY --from=node-builder /app/web/package*.json ./web/
-COPY --from=node-builder /app/web/node_modules ./web/node_modules
-
# Create data directory for SQLite database
RUN mkdir -p /app/data && chown -R appuser:appgroup /app
-# Copy startup script
-COPY docker-entrypoint.sh ./
-RUN chmod +x docker-entrypoint.sh
-
# Environment variables with defaults
ENV PORT=3001
-ENV WEB_PORT=5173
ENV READ_TIMEOUT=600
ENV WRITE_TIMEOUT=600
ENV IDLE_TIMEOUT=600
@@ -75,8 +63,8 @@ ENV ANTHROPIC_VERSION=2023-06-01
ENV ANTHROPIC_MAX_RETRIES=3
ENV DB_PATH=/app/data/requests.db
-# Expose ports
-EXPOSE 3001 5173
+# Expose single port
+EXPOSE 3001
# Switch to app user
USER appuser
@@ -85,5 +73,5 @@ USER appuser
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost:3001/health > /dev/null || exit 1
-# Start both services
-CMD ["./docker-entrypoint.sh"]
\ No newline at end of file
+# Start single binary
+CMD ["./bin/proxy"]
diff --git a/Makefile b/Makefile
index e4d75c50f..186d4db4b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: all build run clean install dev
+.PHONY: all build run clean install dev
# Default target
all: install build
@@ -10,17 +10,22 @@ install:
@echo "📦 Installing Node dependencies..."
cd web && npm install
-# Build both services
-build: build-proxy build-web
+# Build single binary (frontend → copy → Go)
+build: build-web copy-frontend build-proxy
build-proxy:
@echo "🔨 Building proxy server..."
- cd proxy && go build -o ../bin/proxy cmd/proxy/main.go
+ cd proxy && CGO_ENABLED=1 go build -o ../bin/proxy cmd/proxy/main.go
build-web:
@echo "🔨 Building web interface..."
cd web && npm run build
+copy-frontend:
+ @echo "📋 Copying frontend assets into Go embed directory..."
+ rm -rf proxy/frontend/dist
+ cp -r web/build/client proxy/frontend/dist
+
# Run in development mode
dev:
@echo "🚀 Starting development servers..."
@@ -40,6 +45,9 @@ clean:
rm -rf bin/
rm -rf web/build/
rm -rf web/.cache/
+ rm -rf proxy/frontend/dist
+ mkdir -p proxy/frontend/dist
+ touch proxy/frontend/dist/.gitkeep
rm -f requests.db
rm -rf requests/
@@ -53,7 +61,7 @@ db-reset:
help:
@echo "Claude Code Monitor - Available targets:"
@echo " make install - Install all dependencies"
- @echo " make build - Build both services"
+ @echo " make build - Build single binary (frontend + Go)"
@echo " make dev - Run in development mode"
@echo " make run-proxy - Run proxy server only"
@echo " make run-web - Run web interface only"
diff --git a/proxy/cmd/proxy/main.go b/proxy/cmd/proxy/main.go
index 8623203b6..4eb2bddd8 100644
--- a/proxy/cmd/proxy/main.go
+++ b/proxy/cmd/proxy/main.go
@@ -2,16 +2,19 @@ package main
import (
"context"
+ "io/fs"
"log"
"net/http"
"os"
"os/signal"
+ "strings"
"syscall"
"time"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
+ "github.com/seifghazi/claude-code-monitor/frontend"
"github.com/seifghazi/claude-code-monitor/internal/config"
"github.com/seifghazi/claude-code-monitor/internal/handler"
"github.com/seifghazi/claude-code-monitor/internal/middleware"
@@ -62,15 +65,45 @@ func main() {
r.HandleFunc("/v1/models", h.Models).Methods("GET")
r.HandleFunc("/health", h.Health).Methods("GET")
- r.HandleFunc("/", h.UI).Methods("GET")
- r.HandleFunc("/ui", h.UI).Methods("GET")
r.HandleFunc("/api/requests", h.GetRequests).Methods("GET")
r.HandleFunc("/api/requests", h.DeleteRequests).Methods("DELETE")
r.HandleFunc("/api/conversations", h.GetConversations).Methods("GET")
r.HandleFunc("/api/conversations/{id}", h.GetConversationByID).Methods("GET")
r.HandleFunc("/api/conversations/project", h.GetConversationsByProject).Methods("GET")
- r.NotFoundHandler = http.HandlerFunc(h.NotFound)
+ // Serve embedded frontend assets
+ distFS, err := fs.Sub(frontend.Assets, "dist")
+ if err != nil {
+ logger.Fatalf("❌ Failed to create sub filesystem: %v", err)
+ }
+ fileServer := http.FileServer(http.FS(distFS))
+
+ // Serve static assets (JS, CSS, fonts, etc.)
+ r.PathPrefix("/assets/").Handler(fileServer)
+
+ // SPA fallback: serve index.html for all non-API, non-asset paths
+ r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ // Let API routes return 404 normally
+ if strings.HasPrefix(req.URL.Path, "/api/") || strings.HasPrefix(req.URL.Path, "/v1/") {
+ h.NotFound(w, req)
+ return
+ }
+ // Try to serve the file directly first (e.g. favicon.ico)
+ f, err := distFS.Open(strings.TrimPrefix(req.URL.Path, "/"))
+ if err == nil {
+ f.Close()
+ fileServer.ServeHTTP(w, req)
+ return
+ }
+ // Fall back to index.html for SPA routing
+ indexHTML, err := fs.ReadFile(distFS, "index.html")
+ if err != nil {
+ h.NotFound(w, req)
+ return
+ }
+ w.Header().Set("Content-Type", "text/html")
+ w.Write(indexHTML)
+ })
srv := &http.Server{
Addr: ":" + cfg.Server.Port,
@@ -80,6 +113,26 @@ func main() {
IdleTimeout: cfg.Server.IdleTimeout,
}
+ // Detect HTTP/HTTPS proxy from environment
+ proxyEnvVars := []string{"HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "NO_PROXY", "no_proxy"}
+ var detectedProxy bool
+ for _, env := range proxyEnvVars {
+ if val := os.Getenv(env); val != "" {
+ if !detectedProxy {
+ logger.Println("⚠️ HTTP/HTTPS proxy detected! All outbound API requests will be routed through:")
+ detectedProxy = true
+ }
+ logger.Printf(" %s=%s", env, val)
+ }
+ }
+ if !detectedProxy {
+ logger.Println("⚠️ No HTTP/HTTPS proxy detected. If you are behind a firewall, API calls may fail!")
+ logger.Println(" Set proxy environment variables before starting:")
+ logger.Printf(" export HTTP_PROXY=http://your-proxy:port")
+ logger.Printf(" export HTTPS_PROXY=http://your-proxy:port")
+ logger.Printf(" export NO_PROXY=localhost,127.0.0.1")
+ }
+
go func() {
logger.Printf("🚀 Claude Code Monitor Server running on http://localhost:%s", cfg.Server.Port)
logger.Printf("📡 API endpoints available at:")
@@ -89,6 +142,11 @@ func main() {
logger.Printf("🎨 Web UI available at:")
logger.Printf(" - GET http://localhost:%s/ (Request Visualizer)", cfg.Server.Port)
logger.Printf(" - GET http://localhost:%s/api/requests (Request API)", cfg.Server.Port)
+ logger.Println("")
+ logger.Println("👉 To use with Claude Code, run:")
+ logger.Printf(" export ANTHROPIC_BASE_URL=http://localhost:%s", cfg.Server.Port)
+ logger.Println(" claude")
+ logger.Println("")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatalf("❌ Server failed to start: %v", err)
diff --git a/proxy/frontend/embed.go b/proxy/frontend/embed.go
new file mode 100644
index 000000000..fccd9d5de
--- /dev/null
+++ b/proxy/frontend/embed.go
@@ -0,0 +1,6 @@
+package frontend
+
+import "embed"
+
+//go:embed all:dist
+var Assets embed.FS
diff --git a/proxy/internal/handler/handlers.go b/proxy/internal/handler/handlers.go
index 05d1360b7..a9bc9bc78 100644
--- a/proxy/internal/handler/handlers.go
+++ b/proxy/internal/handler/handlers.go
@@ -10,7 +10,6 @@ import (
"io"
"log"
"net/http"
- "os"
"sort"
"strconv"
"strings"
@@ -60,6 +59,7 @@ func (h *Handler) Messages(w http.ResponseWriter, r *http.Request) {
var req model.AnthropicRequest
if err := json.Unmarshal(bodyBytes, &req); err != nil {
log.Printf("❌ Error parsing JSON: %v", err)
+ log.Printf("📋 Raw request body: %s", string(bodyBytes))
writeErrorResponse(w, "Invalid JSON", http.StatusBadRequest)
return
}
@@ -150,18 +150,6 @@ func (h *Handler) Health(w http.ResponseWriter, r *http.Request) {
writeJSONResponse(w, response)
}
-func (h *Handler) UI(w http.ResponseWriter, r *http.Request) {
- htmlContent, err := os.ReadFile("index.html")
- if err != nil {
- // Error reading index.html
- http.Error(w, "UI not available", http.StatusNotFound)
- return
- }
-
- w.Header().Set("Content-Type", "text/html")
- w.Write(htmlContent)
-}
-
func (h *Handler) GetRequests(w http.ResponseWriter, r *http.Request) {
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 {
diff --git a/proxy/internal/model/models.go b/proxy/internal/model/models.go
index a5d03c058..4a86aec37 100644
--- a/proxy/internal/model/models.go
+++ b/proxy/internal/model/models.go
@@ -147,6 +147,41 @@ type AnthropicRequest struct {
ToolChoice interface{} `json:"tool_choice,omitempty"`
}
+func (r *AnthropicRequest) UnmarshalJSON(data []byte) error {
+ // Use an alias to avoid infinite recursion
+ type Alias AnthropicRequest
+ aux := &struct {
+ System json.RawMessage `json:"system,omitempty"`
+ *Alias
+ }{
+ Alias: (*Alias)(r),
+ }
+
+ if err := json.Unmarshal(data, aux); err != nil {
+ return err
+ }
+
+ if len(aux.System) == 0 {
+ return nil
+ }
+
+ // Try as array first
+ var systemMessages []AnthropicSystemMessage
+ if err := json.Unmarshal(aux.System, &systemMessages); err == nil {
+ r.System = systemMessages
+ return nil
+ }
+
+ // Try as string
+ var systemStr string
+ if err := json.Unmarshal(aux.System, &systemStr); err == nil {
+ r.System = []AnthropicSystemMessage{{Type: "text", Text: systemStr}}
+ return nil
+ }
+
+ return json.Unmarshal(aux.System, &r.System)
+}
+
type ModelsResponse struct {
Object string `json:"object"`
Data []ModelInfo `json:"data"`
diff --git a/web/app/entry.server.tsx b/web/app/entry.server.tsx
deleted file mode 100644
index 45db3229c..000000000
--- a/web/app/entry.server.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-/**
- * By default, Remix will handle generating the HTTP Response for you.
- * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
- * For more information, see https://remix.run/file-conventions/entry.server
- */
-
-import { PassThrough } from "node:stream";
-
-import type { AppLoadContext, EntryContext } from "@remix-run/node";
-import { createReadableStreamFromReadable } from "@remix-run/node";
-import { RemixServer } from "@remix-run/react";
-import { isbot } from "isbot";
-import { renderToPipeableStream } from "react-dom/server";
-
-const ABORT_DELAY = 5_000;
-
-export default function handleRequest(
- request: Request,
- responseStatusCode: number,
- responseHeaders: Headers,
- remixContext: EntryContext,
- // This is ignored so we can keep it in the template for visibility. Feel
- // free to delete this parameter in your app if you're not using it!
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- loadContext: AppLoadContext
-) {
- return isbot(request.headers.get("user-agent") || "")
- ? handleBotRequest(
- request,
- responseStatusCode,
- responseHeaders,
- remixContext
- )
- : handleBrowserRequest(
- request,
- responseStatusCode,
- responseHeaders,
- remixContext
- );
-}
-
-function handleBotRequest(
- request: Request,
- responseStatusCode: number,
- responseHeaders: Headers,
- remixContext: EntryContext
-) {
- return new Promise((resolve, reject) => {
- let shellRendered = false;
- const { pipe, abort } = renderToPipeableStream(
- ,
- {
- onAllReady() {
- shellRendered = true;
- const body = new PassThrough();
- const stream = createReadableStreamFromReadable(body);
-
- responseHeaders.set("Content-Type", "text/html");
-
- resolve(
- new Response(stream, {
- headers: responseHeaders,
- status: responseStatusCode,
- })
- );
-
- pipe(body);
- },
- onShellError(error: unknown) {
- reject(error);
- },
- onError(error: unknown) {
- responseStatusCode = 500;
- // Log streaming rendering errors from inside the shell. Don't log
- // errors encountered during initial shell rendering since they'll
- // reject and get logged in handleDocumentRequest.
- if (shellRendered) {
- console.error(error);
- }
- },
- }
- );
-
- setTimeout(abort, ABORT_DELAY);
- });
-}
-
-function handleBrowserRequest(
- request: Request,
- responseStatusCode: number,
- responseHeaders: Headers,
- remixContext: EntryContext
-) {
- return new Promise((resolve, reject) => {
- let shellRendered = false;
- const { pipe, abort } = renderToPipeableStream(
- ,
- {
- onShellReady() {
- shellRendered = true;
- const body = new PassThrough();
- const stream = createReadableStreamFromReadable(body);
-
- responseHeaders.set("Content-Type", "text/html");
-
- resolve(
- new Response(stream, {
- headers: responseHeaders,
- status: responseStatusCode,
- })
- );
-
- pipe(body);
- },
- onShellError(error: unknown) {
- reject(error);
- },
- onError(error: unknown) {
- responseStatusCode = 500;
- // Log streaming rendering errors from inside the shell. Don't log
- // errors encountered during initial shell rendering since they'll
- // reject and get logged in handleDocumentRequest.
- if (shellRendered) {
- console.error(error);
- }
- },
- }
- );
-
- setTimeout(abort, ABORT_DELAY);
- });
-}
diff --git a/web/app/routes/api.conversations.tsx b/web/app/routes/api.conversations.tsx
deleted file mode 100644
index 988a84cbc..000000000
--- a/web/app/routes/api.conversations.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import type { LoaderFunction } from "@remix-run/node";
-import { json } from "@remix-run/node";
-
-export const loader: LoaderFunction = async ({ request }) => {
- try {
- const url = new URL(request.url);
- const modelFilter = url.searchParams.get("model");
-
- const backendUrl = new URL('http://localhost:3001/api/conversations');
- if (modelFilter) {
- backendUrl.searchParams.append('model', modelFilter);
- }
-
- const response = await fetch(backendUrl.toString());
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const data = await response.json();
- return json(data);
- } catch (error) {
- console.error('Failed to fetch conversations:', error);
- return json({ conversations: [] });
- }
-};
\ No newline at end of file
diff --git a/web/app/routes/api.grade-prompt.tsx b/web/app/routes/api.grade-prompt.tsx
deleted file mode 100644
index 4a84253d5..000000000
--- a/web/app/routes/api.grade-prompt.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import type { ActionFunction } from "@remix-run/node";
-import { json } from "@remix-run/node";
-
-export const action: ActionFunction = async ({ request }) => {
- if (request.method !== "POST") {
- return json({ error: 'Method not allowed' }, { status: 405 });
- }
-
- try {
- const body = await request.json();
-
- // Forward the request to the Go backend
- const response = await fetch('http://localhost:3001/api/grade-prompt', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(body)
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const data = await response.json();
- return json(data);
- } catch (error) {
- console.error('Failed to grade prompt:', error);
- return json({
- error: 'Failed to grade prompt. Please ensure the backend is running and has a valid Anthropic API key.'
- }, { status: 500 });
- }
-};
\ No newline at end of file
diff --git a/web/app/routes/api.requests.tsx b/web/app/routes/api.requests.tsx
deleted file mode 100644
index 7f2b9cf52..000000000
--- a/web/app/routes/api.requests.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import type { ActionFunction, LoaderFunction } from "@remix-run/node";
-import { json } from "@remix-run/node";
-
-export const loader: LoaderFunction = async ({ request }) => {
- try {
- const url = new URL(request.url);
- const modelFilter = url.searchParams.get("model");
- const page = url.searchParams.get("page");
- const limit = url.searchParams.get("limit");
-
- // Forward the request to the Go backend
- const backendUrl = new URL('http://localhost:3001/api/requests');
- if (modelFilter) {
- backendUrl.searchParams.append('model', modelFilter);
- }
- if (page) {
- backendUrl.searchParams.append('page', page);
- }
- if (limit) {
- backendUrl.searchParams.append('limit', limit);
- }
-
- const response = await fetch(backendUrl.toString());
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const data = await response.json();
- return json(data);
- } catch (error) {
- console.error('Failed to fetch requests:', error);
-
- // Return empty array if backend is not available
- return json({ requests: [] });
- }
-};
-
-export const action: ActionFunction = async ({ request }) => {
- const method = request.method;
-
- if (method === "DELETE") {
- try {
- // Forward the DELETE request to the Go backend
- const response = await fetch('http://localhost:3001/api/requests', {
- method: 'DELETE'
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- return json({ success: true });
- } catch (error) {
- console.error('Failed to clear requests:', error);
- return json({ success: false, error: 'Failed to clear requests' }, { status: 500 });
- }
- }
-
- return json({ error: 'Method not allowed' }, { status: 405 });
-};
\ No newline at end of file
diff --git a/web/vite.config.ts b/web/vite.config.ts
index ef6d1387a..1d0c9370c 100644
--- a/web/vite.config.ts
+++ b/web/vite.config.ts
@@ -2,15 +2,10 @@ import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
-declare module "@remix-run/node" {
- interface Future {
- v3_singleFetch: true;
- }
-}
-
export default defineConfig({
plugins: [
remix({
+ ssr: false,
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,