Skip to content
Merged
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
10 changes: 0 additions & 10 deletions scripts/startup.sh
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
#!/bin/bash
set -e

# Create runtime config with environment variables
mkdir -p /app/frontend/dist/assets
cat > /app/frontend/dist/assets/runtime-config.js <<EOL
window.RUNTIME_CONFIG = {
CODER_URL: "${CODER_URL}",
VITE_PUBLIC_POSTHOG_KEY: "${VITE_PUBLIC_POSTHOG_KEY}",
VITE_PUBLIC_POSTHOG_HOST: "${VITE_PUBLIC_POSTHOG_HOST}"
};
EOL

# Start the application
exec uvicorn main:app --host 0.0.0.0 --port 8000 --workers $API_WORKERS
2 changes: 2 additions & 0 deletions src/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from routers.workspace_router import workspace_router
from routers.pad_router import pad_router
from routers.template_pad_router import template_pad_router
from routers.app_router import app_router
from database.service import TemplatePadService
from database.database import async_session, run_migrations_with_lock

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

if __name__ == "__main__":
import uvicorn
Expand Down
35 changes: 35 additions & 0 deletions src/backend/routers/app_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import os
import json
import time

from fastapi import APIRouter
from config import STATIC_DIR

app_router = APIRouter()

@app_router.get("/build-info")
async def get_build_info():
"""
Return the current build information from the static assets
"""
try:
# Read the build-info.json file that will be generated during build
build_info_path = os.path.join(STATIC_DIR, "build-info.json")
with open(build_info_path, 'r') as f:
build_info = json.load(f)
return build_info
except Exception as e:
# Return a default response if file doesn't exist
print(f"Error reading build-info.json: {str(e)}")
return {"buildHash": "development", "timestamp": int(time.time())}

@app_router.get("/config")
async def get_app_config():
"""
Return runtime configuration for the frontend
"""
return {
"coderUrl": os.getenv("CODER_URL", ""),
"posthogKey": os.getenv("VITE_PUBLIC_POSTHOG_KEY", ""),
"posthogHost": os.getenv("VITE_PUBLIC_POSTHOG_HOST", "")
}
3 changes: 3 additions & 0 deletions src/backend/routers/workspace_router.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import os
import json
import time

from pydantic import BaseModel
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse

from dependencies import UserSession, require_auth, get_coder_api
from coder import CoderAPI
from config import STATIC_DIR

workspace_router = APIRouter()

Expand Down
3 changes: 1 addition & 2 deletions src/frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<meta name="theme-color" content="#untime0" />

<title>Pad.ws</title>
<link rel="icon" type="image/x-icon" href="/assets/images/favicon.png" />
<script src="/assets/runtime-config.js"></script>
</head>

<body>
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type * as TExcalidraw from "@atyrode/excalidraw";

import App from "./src/App";
import AuthGate from "./src/AuthGate";
import { BuildVersionCheck } from "./src/BuildVersionCheck";


declare global {
Expand All @@ -31,6 +32,7 @@ async function initApp() {
<StrictMode>
<PostHogProvider client={posthog}>
<QueryClientProvider client={queryClient}>
<BuildVersionCheck />
<AuthGate>
<App
useCustom={(api: any, args?: any[]) => { }}
Expand Down
51 changes: 34 additions & 17 deletions src/frontend/src/AuthGate.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from "react";
import { useAuthCheck } from "./api/hooks";
import { getAppConfig } from "./api/configService";

/**
* If unauthenticated, it shows the AuthModal as an overlay, but still renders the app behind it.
Expand All @@ -19,26 +20,42 @@ export default function AuthGate({ children }: { children: React.ReactNode }) {
useEffect(() => {
// Only run the Coder OIDC priming once per session, after auth is confirmed
if (isAuthenticated === true && !coderAuthDone) {
const iframe = document.createElement("iframe");
iframe.style.display = "none";
// Use runtime config if available, fall back to import.meta.env
const coderUrl = window.RUNTIME_CONFIG?.CODER_URL || import.meta.env.CODER_URL;
iframe.src = `${coderUrl}/api/v2/users/oidc/callback`;
console.debug(`[pad.ws] (Silently) Priming Coder OIDC session for ${coderUrl}`);
const setupIframe = async () => {
try {
// Get config from API
const config = await getAppConfig();

if (!config.coderUrl) {
console.warn('[pad.ws] Coder URL not found, skipping OIDC priming');
setCoderAuthDone(true);
return;
}

const iframe = document.createElement("iframe");
iframe.style.display = "none";
iframe.src = `${config.coderUrl}/api/v2/users/oidc/callback`;
console.debug(`[pad.ws] (Silently) Priming Coder OIDC session for ${config.coderUrl}`);

// Remove iframe as soon as it loads, or after 2s fallback
const cleanup = () => {
if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
setCoderAuthDone(true);
};

iframe.onload = cleanup;
document.body.appendChild(iframe);
iframeRef.current = iframe;
// Remove iframe as soon as it loads, or after fallback timeout
const cleanup = () => {
if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
setCoderAuthDone(true);
};

// Fallback: remove iframe after 5s if onload doesn't fire
timeoutRef.current = window.setTimeout(cleanup, 5000);
iframe.onload = cleanup;
document.body.appendChild(iframe);
iframeRef.current = iframe;

// Fallback: remove iframe after 5s if onload doesn't fire
timeoutRef.current = window.setTimeout(cleanup, 5000);
} catch (error) {
console.error('[pad.ws] Error setting up Coder OIDC priming:', error);
setCoderAuthDone(true);
}
};

setupIframe();

// Cleanup on unmount or re-run
return () => {
if (iframeRef.current && iframeRef.current.parentNode) {
Expand Down
61 changes: 61 additions & 0 deletions src/frontend/src/BuildVersionCheck.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useEffect, useState, useCallback } from 'react';
import { useBuildInfo, useSaveCanvas } from './api/hooks';
import { saveCurrentCanvas } from './utils/canvasUtils';

/**
* Component that checks for application version changes and refreshes the page when needed.
* This component doesn't render anything visible.
*/
export function BuildVersionCheck() {
// Store the initial build hash when the component first loads
const [initialBuildHash, setInitialBuildHash] = useState<string | null>(null);

// Query for the current build info from the server
const { data: buildInfo } = useBuildInfo();

// Get the saveCanvas mutation
const { mutate: saveCanvas } = useSaveCanvas({
onSuccess: () => {
console.debug("[pad.ws] Canvas saved before refresh");
// Refresh the page immediately after saving
window.location.reload();
},
onError: (error) => {
console.error("[pad.ws] Failed to save canvas before refresh:", error);
// Refresh anyway even if save fails
window.location.reload();
}
});

// Function to handle version update
const handleVersionUpdate = useCallback(() => {
// Save the canvas and then refresh
saveCurrentCanvas(
saveCanvas,
undefined, // No success callback needed as it's handled in the useSaveCanvas hook
() => window.location.reload() // On error, just refresh
);
}, [saveCanvas]);

useEffect(() => {
// On first load, store the initial build hash
if (buildInfo?.buildHash && initialBuildHash === null) {
console.log('Initial build hash:', buildInfo.buildHash);
setInitialBuildHash(buildInfo.buildHash);
}

// If we have both values and they don't match, a new version is available
if (initialBuildHash !== null &&
buildInfo?.buildHash &&
initialBuildHash !== buildInfo.buildHash) {

console.log('New version detected. Current:', initialBuildHash, 'New:', buildInfo.buildHash);

// Save the canvas and then refresh
handleVersionUpdate();
}
}, [buildInfo, initialBuildHash, handleVersionUpdate]);

// This component doesn't render anything
return null;
}
39 changes: 39 additions & 0 deletions src/frontend/src/api/configService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { fetchApi } from './apiUtils';

/**
* Application configuration interface
*/
export interface AppConfig {
coderUrl: string;
posthogKey: string;
posthogHost: string;
}

// Cache the config to avoid unnecessary API calls
let cachedConfig: AppConfig | null = null;

/**
* Get the application configuration from the API
* @returns The application configuration
*/
export async function getAppConfig(): Promise<AppConfig> {
// Return cached config if available
if (cachedConfig) {
return cachedConfig;
}

try {
// Fetch config from API
const config = await fetchApi('/api/app/config');
cachedConfig = config;
return config;
} catch (error) {
console.error('[pad.ws] Failed to load application configuration:', error);
// Return default values as fallback
return {
coderUrl: '',
posthogKey: '',
posthogHost: ''
};
}
}
24 changes: 24 additions & 0 deletions src/frontend/src/api/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export interface CanvasBackupsResponse {
backups: CanvasBackup[];
}

export interface BuildInfo {
buildHash: string;
timestamp: number;
}

// API functions
export const api = {
// Authentication
Expand Down Expand Up @@ -134,6 +139,16 @@ export const api = {
throw error;
}
},

// Build Info
getBuildInfo: async (): Promise<BuildInfo> => {
try {
const result = await fetchApi('/api/app/build-info');
return result;
} catch (error) {
throw error;
}
},
};

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

export function useBuildInfo(options?: UseQueryOptions<BuildInfo>) {
return useQuery({
queryKey: ['buildInfo'],
queryFn: api.getBuildInfo,
refetchInterval: 60000, // Check every minute
...options,
});
}

// Mutation hooks
export function useStartWorkspace(options?: UseMutationOptions) {
return useMutation({
Expand Down
5 changes: 0 additions & 5 deletions src/frontend/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
interface Window {
RUNTIME_CONFIG?: {
CODER_URL: string;
VITE_PUBLIC_POSTHOG_KEY: string;
VITE_PUBLIC_POSTHOG_HOST: string;
};
ExcalidrawLib: any;
}
Loading