Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app-staging.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
runtime: python313
service: ocotillo-staging
handlers:
# version.json must never be cached so the new-version polling is reliable
- url: /version.json
static_files: dist/version.json
upload: dist/version.json
secure: always
http_headers:
Cache-Control: no-store
# Serve all static files with url ending with a file extension
- url: /(.*\..+)$
static_files: dist/\1
Expand Down
7 changes: 7 additions & 0 deletions app.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
runtime: python313
service: ocotillo
handlers:
# version.json must never be cached so the new-version polling is reliable
- url: /version.json
static_files: dist/version.json
upload: dist/version.json
secure: always
http_headers:
Cache-Control: no-store
# Serve all static files with url ending with a file extension
- url: /(.*\..+)$
static_files: dist/\1
Expand Down
1 change: 1 addition & 0 deletions public/version.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"buildTime":"dev"}
14 changes: 11 additions & 3 deletions src/components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ import { ReportBugButton } from '@/components/Button'
import { AmpRole, PRIMARY_NAV, RESOURCE_NAV, type NavItem } from '@/config/navigation'
import { useAccessCapabilities } from '@/hooks'
import { useSearch } from '@/providers/search-provider'
import { NewVersionBanner } from '@/components/NewVersionBanner'
import pkg from '../../package.json'

// Support panel state shared between the sidebar footer button and the panel itself
export const SupportPanelContext = createContext<{
Expand Down Expand Up @@ -353,6 +355,14 @@ function AppSidebar() {

{/* Help & Support button — hidden until panel content is defined */}
{/* <SupportPanelTrigger collapsed={collapsed} /> */}

{!collapsed && (
<div className="px-3 pb-2">
<span className="text-xs text-muted-foreground/60">
v{pkg.version}
</span>
</div>
)}
</SidebarFooter>
</Sidebar>
)
Expand Down Expand Up @@ -720,6 +730,7 @@ function AppShellInner({ children }: { children?: React.ReactNode }) {
<SidebarAutoCollapse />
<AppSidebar />
<AppContent className="min-w-0">
<NewVersionBanner />
<ShellHeader />
<div className="flex-1 min-h-0 overflow-y-auto">
{children ?? <Outlet />}
Expand All @@ -732,9 +743,6 @@ function AppShellInner({ children }: { children?: React.ReactNode }) {

export const AppShell = ({ children }: { children?: React.ReactNode }) => {
return (
// h-svh + overflow-hidden pins the shell to exactly the viewport so no page
// can cause a body-level scroll. AppContent gets overflow-y-auto so regular
// pages still scroll within the frame.
<AppLayout className="h-svh overflow-hidden">
<AppShellInner>{children}</AppShellInner>
</AppLayout>
Expand Down
24 changes: 24 additions & 0 deletions src/components/NewVersionBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Button } from '@/components/ui/button'
import { useNewVersion } from '@/hooks/useNewVersion'

export const NewVersionBanner = () => {
const { isNewVersionAvailable } = useNewVersion()

if (!isNewVersionAvailable) return null

return (
<div className="flex items-center justify-between gap-4 px-4 py-2.5 bg-indigo-500 text-white text-sm shrink-0">
<span className="font-medium">
We just made Ocotillo 5% better. Refresh to get the good stuff!
</span>
<Button
variant="outline"
size="sm"
onClick={() => window.location.reload()}
className="border-white/60 bg-white/10 text-white hover:bg-white/20 hover:text-white"
>
Refresh Now
</Button>
</div>
)
}
59 changes: 59 additions & 0 deletions src/hooks/useNewVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useEffect, useRef, useState } from 'react'

const POLL_INTERVAL_MS = 5 * 60 * 1000 // 5 minutes

async function fetchBuildTime(): Promise<string | null> {
try {
const res = await fetch('/version.json', { cache: 'no-store' })
if (!res.ok) return null
const data = await res.json()
return data?.buildTime ?? null
} catch {
return null
}
}

/**
* Polls /version.json every 5 minutes. Returns true once the server's
* buildTime differs from the value captured when the app first loaded,
* indicating that a new version has been deployed.
*
* Append ?preview-refresh-banner to any URL to force the banner visible for
* design review or screenshots.
*/
export const useNewVersion = () => {
const forceShow = new URLSearchParams(window.location.search).has(
'preview-refresh-banner'
)
const [isNewVersionAvailable, setIsNewVersionAvailable] = useState(forceShow)
const initialBuildTime = useRef<string | null>(null)

useEffect(() => {
let cancelled = false

const check = async () => {
const buildTime = await fetchBuildTime()
if (cancelled) return

if (initialBuildTime.current === null) {
// First fetch — record the version the user loaded with.
initialBuildTime.current = buildTime
return
}

if (buildTime !== null && buildTime !== initialBuildTime.current) {
setIsNewVersionAvailable(true)
}
}

check()
const timer = setInterval(check, POLL_INTERVAL_MS)

return () => {
cancelled = true
clearInterval(timer)
}
}, [])

return { isNewVersionAvailable }
}
13 changes: 13 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
import { sentryVitePlugin } from '@sentry/vite-plugin'
import tailwindcss from '@tailwindcss/vite'
import { writeFileSync } from 'fs'
import { resolve } from 'path'

export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
Expand All @@ -28,6 +30,17 @@ export default defineConfig(({ mode }) => {
dedupe: ['react', 'react-dom', 'scheduler', 'use-sync-external-store'],
},
plugins: [
// Write public/version.json on every build so polling clients can detect
// that a new version has been deployed and prompt users to reload.
{
name: 'write-version-json',
buildStart() {
writeFileSync(
resolve(__dirname, 'public/version.json'),
JSON.stringify({ buildTime: new Date().toISOString() })
)
},
},
tailwindcss(),
react(),
tsconfigPaths(),
Expand Down
Loading