Skip to content

Commit 8517627

Browse files
committed
feat: add app router and build info endpoint
- Introduced a new router for application-related endpoints, including a `/build-info` endpoint to serve current build information from a generated JSON file. - Implemented a Vite plugin to generate `build-info.json` during the build process, containing a unique build hash and timestamp. - Added a `BuildVersionCheck` component in the frontend to monitor build version changes and refresh the application when a new version is detected. - Updated existing routers to include necessary imports for the new functionality.
1 parent b3963a0 commit 8517627

File tree

8 files changed

+206
-1
lines changed

8 files changed

+206
-1
lines changed

src/backend/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from routers.workspace_router import workspace_router
1818
from routers.pad_router import pad_router
1919
from routers.template_pad_router import template_pad_router
20+
from routers.app_router import app_router
2021
from database.service import TemplatePadService
2122
from database.database import async_session, run_migrations_with_lock
2223

@@ -140,6 +141,7 @@ async def read_root(request: Request, auth: Optional[UserSession] = Depends(opti
140141
app.include_router(workspace_router, prefix="/api/workspace")
141142
app.include_router(pad_router, prefix="/api/pad")
142143
app.include_router(template_pad_router, prefix="/api/templates")
144+
app.include_router(app_router, prefix="/api/app")
143145

144146
if __name__ == "__main__":
145147
import uvicorn

src/backend/routers/app_router.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import os
2+
import json
3+
import time
4+
5+
from fastapi import APIRouter
6+
from config import STATIC_DIR
7+
8+
app_router = APIRouter()
9+
10+
@app_router.get("/build-info")
11+
async def get_build_info():
12+
"""
13+
Return the current build information from the static assets
14+
"""
15+
try:
16+
# Read the build-info.json file that will be generated during build
17+
build_info_path = os.path.join(STATIC_DIR, "build-info.json")
18+
with open(build_info_path, 'r') as f:
19+
build_info = json.load(f)
20+
return build_info
21+
except Exception as e:
22+
# Return a default response if file doesn't exist
23+
print(f"Error reading build-info.json: {str(e)}")
24+
return {"buildHash": "development", "timestamp": int(time.time())}

src/backend/routers/workspace_router.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import os
2+
import json
3+
import time
24

35
from pydantic import BaseModel
46
from fastapi import APIRouter, Depends, HTTPException
57
from fastapi.responses import JSONResponse
68

79
from dependencies import UserSession, require_auth, get_coder_api
810
from coder import CoderAPI
11+
from config import STATIC_DIR
912

1013
workspace_router = APIRouter()
1114

src/frontend/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type * as TExcalidraw from "@atyrode/excalidraw";
1515

1616
import App from "./src/App";
1717
import AuthGate from "./src/AuthGate";
18+
import { BuildVersionCheck } from "./src/BuildVersionCheck";
1819

1920

2021
declare global {
@@ -31,6 +32,7 @@ async function initApp() {
3132
<StrictMode>
3233
<PostHogProvider client={posthog}>
3334
<QueryClientProvider client={queryClient}>
35+
<BuildVersionCheck />
3436
<AuthGate>
3537
<App
3638
useCustom={(api: any, args?: any[]) => { }}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { useEffect, useState, useCallback } from 'react';
2+
import { useBuildInfo, useSaveCanvas } from './api/hooks';
3+
import { saveCurrentCanvas } from './utils/canvasUtils';
4+
5+
/**
6+
* Component that checks for application version changes and refreshes the page when needed.
7+
* This component doesn't render anything visible.
8+
*/
9+
export function BuildVersionCheck() {
10+
// Store the initial build hash when the component first loads
11+
const [initialBuildHash, setInitialBuildHash] = useState<string | null>(null);
12+
13+
// Query for the current build info from the server
14+
const { data: buildInfo } = useBuildInfo();
15+
16+
// Get the saveCanvas mutation
17+
const { mutate: saveCanvas } = useSaveCanvas({
18+
onSuccess: () => {
19+
console.debug("[pad.ws] Canvas saved before refresh");
20+
// Refresh the page immediately after saving
21+
window.location.reload();
22+
},
23+
onError: (error) => {
24+
console.error("[pad.ws] Failed to save canvas before refresh:", error);
25+
// Refresh anyway even if save fails
26+
window.location.reload();
27+
}
28+
});
29+
30+
// Function to handle version update
31+
const handleVersionUpdate = useCallback(() => {
32+
// Save the canvas and then refresh
33+
saveCurrentCanvas(
34+
saveCanvas,
35+
undefined, // No success callback needed as it's handled in the useSaveCanvas hook
36+
() => window.location.reload() // On error, just refresh
37+
);
38+
}, [saveCanvas]);
39+
40+
useEffect(() => {
41+
// On first load, store the initial build hash
42+
if (buildInfo?.buildHash && initialBuildHash === null) {
43+
console.log('Initial build hash:', buildInfo.buildHash);
44+
setInitialBuildHash(buildInfo.buildHash);
45+
}
46+
47+
// If we have both values and they don't match, a new version is available
48+
if (initialBuildHash !== null &&
49+
buildInfo?.buildHash &&
50+
initialBuildHash !== buildInfo.buildHash) {
51+
52+
console.log('New version detected. Current:', initialBuildHash, 'New:', buildInfo.buildHash);
53+
54+
// Save the canvas and then refresh
55+
handleVersionUpdate();
56+
}
57+
}, [buildInfo, initialBuildHash, handleVersionUpdate]);
58+
59+
// This component doesn't render anything
60+
return null;
61+
}

src/frontend/src/api/hooks.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ export interface CanvasBackupsResponse {
3939
backups: CanvasBackup[];
4040
}
4141

42+
export interface BuildInfo {
43+
buildHash: string;
44+
timestamp: number;
45+
}
46+
4247
// API functions
4348
export const api = {
4449
// Authentication
@@ -134,6 +139,16 @@ export const api = {
134139
throw error;
135140
}
136141
},
142+
143+
// Build Info
144+
getBuildInfo: async (): Promise<BuildInfo> => {
145+
try {
146+
const result = await fetchApi('/api/app/build-info');
147+
return result;
148+
} catch (error) {
149+
throw error;
150+
}
151+
},
137152
};
138153

139154
// Query hooks
@@ -192,6 +207,15 @@ export function useCanvasBackups(limit: number = 10, options?: UseQueryOptions<C
192207
});
193208
}
194209

210+
export function useBuildInfo(options?: UseQueryOptions<BuildInfo>) {
211+
return useQuery({
212+
queryKey: ['buildInfo'],
213+
queryFn: api.getBuildInfo,
214+
refetchInterval: 60000, // Check every minute
215+
...options,
216+
});
217+
}
218+
195219
// Mutation hooks
196220
export function useStartWorkspace(options?: UseMutationOptions) {
197221
return useMutation({

src/frontend/src/utils/canvasUtils.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { DEFAULT_SETTINGS } from '../types/settings';
2+
import type { ExcalidrawImperativeAPI } from "@atyrode/excalidraw/types";
3+
import { CanvasData } from '../api/hooks';
24

35
/**
46
*
@@ -42,3 +44,59 @@ export function normalizeCanvasData(data: any) {
4244

4345
return { ...data, appState };
4446
}
47+
48+
/**
49+
* Saves the current canvas state using the Excalidraw API
50+
* @param saveCanvas The saveCanvas mutation function from useSaveCanvas hook
51+
* @param onSuccess Optional callback to run after successful save
52+
* @param onError Optional callback to run if save fails
53+
*/
54+
export function saveCurrentCanvas(
55+
saveCanvas: (data: CanvasData) => void,
56+
onSuccess?: () => void,
57+
onError?: (error: any) => void
58+
) {
59+
try {
60+
// Get the excalidrawAPI from the window object
61+
const excalidrawAPI = (window as any).excalidrawAPI as ExcalidrawImperativeAPI | null;
62+
63+
if (excalidrawAPI) {
64+
// Get the current elements, state, and files
65+
const elements = excalidrawAPI.getSceneElements();
66+
const appState = excalidrawAPI.getAppState();
67+
const files = excalidrawAPI.getFiles();
68+
69+
// Save the canvas data
70+
saveCanvas({
71+
elements: [...elements] as any[], // Convert readonly array to mutable array
72+
appState,
73+
files
74+
});
75+
76+
// Call onSuccess callback if provided
77+
if (onSuccess) {
78+
onSuccess();
79+
}
80+
81+
return true;
82+
} else {
83+
console.warn("[pad.ws] ExcalidrawAPI not available");
84+
85+
// Call onError callback if provided
86+
if (onError) {
87+
onError(new Error("ExcalidrawAPI not available"));
88+
}
89+
90+
return false;
91+
}
92+
} catch (error) {
93+
console.error("[pad.ws] Error saving canvas:", error);
94+
95+
// Call onError callback if provided
96+
if (onError) {
97+
onError(error);
98+
}
99+
100+
return false;
101+
}
102+
}

src/frontend/vite.config.mts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,32 @@
1-
import { defineConfig, loadEnv } from "vite";
1+
import { defineConfig, loadEnv, Plugin } from "vite";
2+
import fs from "fs";
3+
import path from "path";
4+
5+
// Create a plugin to generate build-info.json during build
6+
const generateBuildInfoPlugin = (): Plugin => ({
7+
name: 'generate-build-info',
8+
closeBundle() {
9+
// Generate a unique build hash (timestamp + random string)
10+
const buildInfo = {
11+
buildHash: Date.now().toString(36) + Math.random().toString(36).substring(2),
12+
timestamp: Date.now()
13+
};
14+
15+
// Ensure the dist directory exists
16+
const distDir = path.resolve(__dirname, 'dist');
17+
if (!fs.existsSync(distDir)) {
18+
fs.mkdirSync(distDir, { recursive: true });
19+
}
20+
21+
// Write to the output directory
22+
fs.writeFileSync(
23+
path.resolve(distDir, 'build-info.json'),
24+
JSON.stringify(buildInfo, null, 2)
25+
);
26+
27+
console.log('Generated build-info.json with hash:', buildInfo.buildHash);
28+
}
29+
});
230

331
// https://vitejs.dev/config/
432
export default defineConfig(({ mode }) => {
@@ -24,6 +52,9 @@ export default defineConfig(({ mode }) => {
2452
'import.meta.env.CODER_URL': JSON.stringify(env.CODER_URL),
2553
},
2654
publicDir: "public",
55+
plugins: [
56+
generateBuildInfoPlugin(),
57+
],
2758
optimizeDeps: {
2859
esbuildOptions: {
2960
// Bumping to 2022 due to "Arbitrary module namespace identifier names" not being

0 commit comments

Comments
 (0)