diff --git a/.gitignore b/.gitignore
index a547bf3..50c8dda 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+
+.env
diff --git a/src/App.tsx b/src/App.tsx
index df93e0d..33bd710 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,10 +1,13 @@
import { QueryProvider } from "@/providers/QueryProvider"
+import { ThemeProvider } from "@/providers/ThemeProvider"
import { AppRouter } from "@/router"
export default function App() {
return (
-
-
-
+
+
+
+
+
)
}
diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx
new file mode 100644
index 0000000..152861d
--- /dev/null
+++ b/src/components/ThemeToggle.tsx
@@ -0,0 +1,48 @@
+import { Moon, Sun } from "lucide-react"
+import { useTheme } from "@/providers/ThemeProvider"
+import { Button } from "@/components/ui/button"
+import { cn } from "@/lib/utils"
+
+interface ThemeToggleProps {
+ className?: string
+ showLabel?: boolean
+ variant?: "outline" | "ghost" | "default" | "secondary"
+ size?: "default" | "sm" | "lg" | "icon"
+}
+
+export function ThemeToggle({
+ className,
+ showLabel = false,
+ variant = "outline",
+ size = "icon",
+}: ThemeToggleProps) {
+ const { theme, resolvedTheme, toggleTheme } = useTheme()
+ const isDark = resolvedTheme === "dark"
+
+ return (
+
+ )
+}
diff --git a/src/features/status/components/SystemOverviewCard.tsx b/src/features/status/components/SystemOverviewCard.tsx
new file mode 100644
index 0000000..de0a79b
--- /dev/null
+++ b/src/features/status/components/SystemOverviewCard.tsx
@@ -0,0 +1,232 @@
+import { Activity, AlertTriangle, Server, HardDrive, Cpu, AlertCircle, User, CheckCircle2 } from "lucide-react"
+import { Card, CardContent } from "@/components/ui/card"
+import { Progress } from "@/components/ui/progress"
+import { Skeleton } from "@/components/ui/skeleton"
+import { Badge } from "@/components/ui/badge"
+import { formatBytes } from "@/lib/format"
+import type { StatusResponse } from "../api/status.types"
+
+interface SystemOverviewCardProps {
+ status?: StatusResponse
+ isLoading: boolean
+ error?: Error | null
+}
+
+export function SystemOverviewCard({ status, isLoading, error }: SystemOverviewCardProps) {
+ if (isLoading) {
+ return (
+
+
+
+ {/* Gateway Skeleton */}
+
+
+
+
+
+ {/* Storage Skeleton */}
+
+
+
+
+
+ {/* Multipart Skeleton */}
+
+
+
+
+
+
+
+
+ )
+ }
+
+ if (error || !status) {
+ return (
+
+
+
+
+
+
+
Gateway Service Unavailable
+
+ {error?.message || "Failed to query live status from Cloudflare Worker"}
+
+
+
+
+
+ )
+ }
+
+ const isDegraded = status.gateway.status === "degraded" || !status.drive.connected
+ const drive = status.drive
+ const quota = drive.quota
+ const isUnlimited = quota?.limit == null
+ const percentUsed = quota?.percentUsed ?? 0
+ const isMultipartEnabled = status.gateway.multipartEnabled ?? false
+ const etagStyle = status.gateway.etagStyle ?? "md5"
+
+ return (
+
+ {/* Top accent bar with unified status gradient */}
+
+
+
+
+
+ {/* Section 1: Gateway Engine */}
+
+
+
+ Gateway Engine
+
+
+
+
+
+
+
+
+
+ {isDegraded ? "Degraded" : "Online & Healthy"}
+
+
+
+
+
+ {isDegraded ? (
+
+ ) : (
+
+ )}
+
+ {isDegraded
+ ? drive.error || "Drive connection degraded"
+ : `Edge routing · Region: ${status.gateway.region}`}
+
+
+ {status.gateway.docsEnabled && (
+
+ Docs
+
+ )}
+
+
+
+ {/* Section 2: S3 Storage Backend (Google Drive) */}
+
+
+
+ S3 Storage Backend
+
+
+
+
+
+
+ {!drive.connected ? (
+
+
Google Drive
+
+
+ {drive.error || "Drive connection error"}
+
+
+ ) : (
+
+
+
+ {formatBytes(quota?.usage)}
+
+
+ {isUnlimited ? "Unlimited" : `/ ${formatBytes(quota?.limit)}`}
+
+
+
+ {!isUnlimited && (
+
+
+ )}
+
+ )}
+
+
+
+
+ {drive.account?.email || drive.account?.displayName || "Google Drive"}
+
+
+
+
+ {/* Section 3: Multipart Uploads */}
+
+
+
+ Multipart Uploads
+
+
+
+
+
+
+
+
+ {isMultipartEnabled ? "Enabled" : "Disabled"}
+
+ {etagStyle.toUpperCase()} ETag
+
+
+
+
+
+
+
+ {isMultipartEnabled
+ ? "Durable Objects resumable state"
+ : "ALLOW_MULTIPART disabled"}
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/index.css b/src/index.css
index fe702ae..1f6ba78 100644
--- a/src/index.css
+++ b/src/index.css
@@ -73,6 +73,16 @@
--radius-xl: calc(var(--radius) + 4px);
}
+html {
+ font-size: 16.5px;
+}
+
+@media (min-width: 768px) {
+ html {
+ font-size: 17px;
+ }
+}
+
body {
margin: 0;
min-height: 100vh;
@@ -91,6 +101,12 @@ body {
linear-gradient(to bottom, rgba(99, 102, 241, 0.05) 1px, transparent 1px);
}
+.dark .bg-grid-pattern {
+ background-image:
+ linear-gradient(to right, rgba(99, 102, 241, 0.08) 1px, transparent 1px),
+ linear-gradient(to bottom, rgba(99, 102, 241, 0.08) 1px, transparent 1px);
+}
+
.bg-gradient-brand {
background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%);
}
diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx
index ae390b8..299f52a 100644
--- a/src/pages/Dashboard.tsx
+++ b/src/pages/Dashboard.tsx
@@ -13,11 +13,10 @@ import { useAuth } from "@/features/auth/hooks/useAuth"
import { useStatus } from "@/features/status/hooks/useStatus"
import { useBucketStats } from "@/features/status/hooks/useBucketStats"
import { statusKeys } from "@/features/status/api/status.keys"
-import { GatewayStatusCard } from "@/features/status/components/GatewayStatusCard"
-import { DriveQuotaCard } from "@/features/status/components/DriveQuotaCard"
-import { MultipartStatusCard } from "@/features/status/components/MultipartStatusCard"
+import { SystemOverviewCard } from "@/features/status/components/SystemOverviewCard"
import { BucketStatsTable } from "@/features/status/components/BucketStatsTable"
import { Button } from "@/components/ui/button"
+import { ThemeToggle } from "@/components/ThemeToggle"
import { formatRelativeTime } from "@/lib/format"
export default function Dashboard() {
@@ -62,13 +61,6 @@ export default function Dashboard() {
return (
- {/* Background ambient colorful glow effects */}
-
-
{/* Navigation Header */}
- {/* Status / Quick Overview cards */}
-
-
-
-
-
+ {/* Unified Status / Quick Overview */}
+
{/* Feature Capabilities & Specifications */}
diff --git a/src/pages/SignIn.tsx b/src/pages/SignIn.tsx
index cf139f0..304d5d1 100644
--- a/src/pages/SignIn.tsx
+++ b/src/pages/SignIn.tsx
@@ -1,19 +1,12 @@
import React, { useState } from "react"
import { useNavigate, Navigate } from "react-router"
-import { Eye, EyeOff, Lock, Database, ArrowRight, Loader2, AlertCircle, Sparkles, Shield, Cloud } from "lucide-react"
+import { Eye, EyeOff, Lock, Database, ArrowRight, Loader2, AlertCircle } from "lucide-react"
import { useAuth } from "@/features/auth/hooks/useAuth"
import { ApiError } from "@/lib/api-client"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card"
+import { ThemeToggle } from "@/components/ThemeToggle"
function getErrorMessage(error: Error | null): string | null {
if (!error) return null
@@ -75,134 +68,104 @@ export default function SignIn() {
}
return (
-
- {/* Background ambient colorful glow effects */}
-
-
-
-
+
+ {/* Theme Toggle in top-right */}
+
+
-
- {/* Logo and Brand Header */}
-
-
-
-
-
-
-
-
-
+
+ {/* Brand & Header */}
+
+
+
-
-
-
- Cloudflare Worker & Drive Bridge
-
-
-
- S3 Drive Storage
+
+ S3 Drive Storage
-
- High-performance management console for Google Drive S3 gateway
+
+ Enter your management password to access the console
{/* Sign In Card */}
-
-
-
-
-
- Sign In to Console
-
-
- Enter your gateway access password to unlock management tools
-
-
-
-
)
diff --git a/src/providers/ThemeProvider.tsx b/src/providers/ThemeProvider.tsx
new file mode 100644
index 0000000..c83f804
--- /dev/null
+++ b/src/providers/ThemeProvider.tsx
@@ -0,0 +1,105 @@
+import * as React from "react"
+
+export type Theme = "light" | "dark" | "system"
+
+interface ThemeProviderProps {
+ children: React.ReactNode
+ defaultTheme?: Theme
+ storageKey?: string
+}
+
+interface ThemeContextType {
+ theme: Theme
+ resolvedTheme: "light" | "dark"
+ setTheme: (theme: Theme) => void
+ toggleTheme: () => void
+}
+
+const ThemeContext = React.createContext
(undefined)
+
+export function ThemeProvider({
+ children,
+ defaultTheme = "system",
+ storageKey = "s3-drive-ui-theme",
+}: ThemeProviderProps) {
+ const [theme, setThemeState] = React.useState(() => {
+ try {
+ const stored = localStorage.getItem(storageKey) as Theme | null
+ if (stored === "light" || stored === "dark" || stored === "system") {
+ return stored
+ }
+ } catch {
+ // localStorage may be unavailable in some private browsing modes
+ }
+ return defaultTheme
+ })
+
+ const [resolvedTheme, setResolvedTheme] = React.useState<"light" | "dark">(() => {
+ if (theme === "system") {
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
+ }
+ return theme
+ })
+
+ React.useEffect(() => {
+ const root = document.documentElement
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
+
+ const applyTheme = () => {
+ const currentResolved =
+ theme === "system" ? (mediaQuery.matches ? "dark" : "light") : theme
+
+ setResolvedTheme(currentResolved)
+ root.classList.remove("light", "dark")
+ root.classList.add(currentResolved)
+ root.style.colorScheme = currentResolved
+ }
+
+ applyTheme()
+
+ const listener = () => {
+ if (theme === "system") {
+ applyTheme()
+ }
+ }
+
+ mediaQuery.addEventListener("change", listener)
+ return () => mediaQuery.removeEventListener("change", listener)
+ }, [theme])
+
+ const setTheme = React.useCallback(
+ (newTheme: Theme) => {
+ try {
+ localStorage.setItem(storageKey, newTheme)
+ } catch {
+ // ignore storage write errors
+ }
+ setThemeState(newTheme)
+ },
+ [storageKey]
+ )
+
+ const toggleTheme = React.useCallback(() => {
+ setTheme(resolvedTheme === "dark" ? "light" : "dark")
+ }, [resolvedTheme, setTheme])
+
+ const value = React.useMemo(
+ () => ({
+ theme,
+ resolvedTheme,
+ setTheme,
+ toggleTheme,
+ }),
+ [theme, resolvedTheme, setTheme, toggleTheme]
+ )
+
+ return {children}
+}
+
+export function useTheme(): ThemeContextType {
+ const context = React.useContext(ThemeContext)
+ if (!context) {
+ throw new Error("useTheme must be used within a ThemeProvider")
+ }
+ return context
+}