From 43be7ac83a9ba3e0ad970f4588729ba2ad394702 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Sun, 5 Oct 2025 06:37:12 -0500 Subject: [PATCH] feat: add deployment info modal with keyboard shortcut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add modal component to view deployment information (version, git commit, build time, etc.) accessible via Cmd/Ctrl+Shift+I keyboard shortcut throughout the app. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/web/src/app/globals.css | 21 ++ apps/web/src/components/ClientProviders.tsx | 2 + apps/web/src/components/DeploymentInfo.tsx | 341 ++++++++++++++++++++ 3 files changed, 364 insertions(+) create mode 100644 apps/web/src/components/DeploymentInfo.tsx diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 48572144..9c1bdd23 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -9,4 +9,25 @@ body { line-height: 1.5; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; +} + +/* Dialog animations */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes contentShow { + from { + opacity: 0; + transform: translate(-50%, -48%) scale(0.96); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } } \ No newline at end of file diff --git a/apps/web/src/components/ClientProviders.tsx b/apps/web/src/components/ClientProviders.tsx index edcb55b5..88ae6a92 100644 --- a/apps/web/src/components/ClientProviders.tsx +++ b/apps/web/src/components/ClientProviders.tsx @@ -5,6 +5,7 @@ import { AbacusDisplayProvider } from '@soroban/abacus-react' import { UserProfileProvider } from '@/contexts/UserProfileContext' import { GameModeProvider } from '@/contexts/GameModeContext' import { FullscreenProvider } from '@/contexts/FullscreenContext' +import { DeploymentInfo } from './DeploymentInfo' interface ClientProvidersProps { children: ReactNode @@ -17,6 +18,7 @@ export function ClientProviders({ children }: ClientProvidersProps) { {children} + diff --git a/apps/web/src/components/DeploymentInfo.tsx b/apps/web/src/components/DeploymentInfo.tsx new file mode 100644 index 00000000..55465c9c --- /dev/null +++ b/apps/web/src/components/DeploymentInfo.tsx @@ -0,0 +1,341 @@ +'use client' + +import React, { useState, useEffect } from 'react' +import * as Dialog from '@radix-ui/react-dialog' +import { Info, X, GitBranch, GitCommit, Clock, Package, Server } from 'lucide-react' +import { css } from '../../styled-system/css' +import { vstack, hstack } from '../../styled-system/patterns' + +interface BuildInfo { + version: string + buildTime: string + buildTimestamp: number + git: { + commit: string | null + commitShort: string | null + branch: string | null + tag: string | null + isDirty: boolean + } + environment: string + buildNumber: string | null + nodeVersion: string +} + +export function DeploymentInfo() { + const [open, setOpen] = useState(false) + const [buildInfo, setBuildInfo] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + // Keyboard shortcut: Cmd/Ctrl + Shift + I + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'I') { + e.preventDefault() + setOpen(prev => !prev) + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, []) + + useEffect(() => { + if (open && !buildInfo) { + fetch('/api/build-info') + .then(res => res.json()) + .then(data => { + setBuildInfo(data) + setLoading(false) + }) + .catch(err => { + console.error('Failed to load build info:', err) + setLoading(false) + }) + } + }, [open, buildInfo]) + + const formatTimestamp = (timestamp: number) => { + const date = new Date(timestamp) + return date.toLocaleString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + timeZoneName: 'short' + }) + } + + const getTimeAgo = (timestamp: number) => { + const seconds = Math.floor((Date.now() - timestamp) / 1000) + + const intervals = { + year: 31536000, + month: 2592000, + week: 604800, + day: 86400, + hour: 3600, + minute: 60, + second: 1 + } + + for (const [unit, secondsInUnit] of Object.entries(intervals)) { + const interval = Math.floor(seconds / secondsInUnit) + if (interval >= 1) { + return `${interval} ${unit}${interval === 1 ? '' : 's'} ago` + } + } + return 'just now' + } + + return ( + + + + + + + Deployment Information + + + {loading ? ( +
+ Loading... +
+ ) : buildInfo ? ( +
+ } + label="Version" + value={buildInfo.version} + /> + + } + label="Build Time" + value={ +
+ {formatTimestamp(buildInfo.buildTimestamp)} + + {getTimeAgo(buildInfo.buildTimestamp)} + +
+ } + /> + + {buildInfo.git.branch && ( + } + label="Branch" + value={ + + {buildInfo.git.branch} + {buildInfo.git.isDirty && ( + + (dirty) + + )} + + } + /> + )} + + {buildInfo.git.commitShort && ( + } + label="Commit" + value={ +
+ + {buildInfo.git.commitShort} + + {buildInfo.git.commit && ( + + {buildInfo.git.commit} + + )} +
+ } + /> + )} + + {buildInfo.git.tag && ( + } + label="Tag" + value={ + + {buildInfo.git.tag} + + } + /> + )} + + } + label="Environment" + value={ + + {buildInfo.environment} + + } + /> + + {buildInfo.buildNumber && ( + + )} + + + {buildInfo.nodeVersion} + + } + /> +
+ ) : ( +
+ Failed to load build information +
+ )} + +
+ Press ⌘⇧I or Ctrl⇧I to toggle +
+ + + + +
+
+
+ ) +} + +function InfoRow({ + icon, + label, + value +}: { + icon?: React.ReactNode + label: string + value: React.ReactNode +}) { + return ( +
+
+ {icon} + {label} +
+
+ {value} +
+
+ ) +}