Demo ready

This commit is contained in:
Connor Yoh 2025-07-02 11:01:13 +01:00
parent 1c342b60ba
commit ed618648e0
10 changed files with 140 additions and 33 deletions

View File

@ -26,8 +26,10 @@ if errorlevel 1 (
exit /b 1
)
REM Find the built JAR
for %%f in (build\libs\Stirling-PDF-*.jar) do set STIRLING_JAR=%%f
REM Find the built JAR(s)
echo ▶ Listing all built JAR files in stirling-pdf\build\libs:
dir /b stirling-pdf\build\libs\Stirling-PDF-*.jar
for %%f in (stirling-pdf\build\libs\Stirling-PDF-*.jar) do set STIRLING_JAR=%%f
if not exist "%STIRLING_JAR%" (
echo ❌ No Stirling-PDF JAR found in build/libs/
exit /b 1
@ -43,6 +45,10 @@ echo ▶ Copying JAR to Tauri libs directory...
copy "%STIRLING_JAR%" "frontend\src-tauri\libs\"
echo ✅ JAR copied to frontend\src-tauri\libs\
REM Log out all JAR files now in the Tauri libs directory
echo ▶ Listing all JAR files in frontend\src-tauri\libs after copy:
dir /b frontend\src-tauri\libs\Stirling-PDF-*.jar
echo ▶ Creating custom JRE with jlink...
if exist "frontend\src-tauri\runtime\jre" rmdir /s /q "frontend\src-tauri\runtime\jre"

View File

@ -62,13 +62,13 @@ fi
print_step "Building Stirling-PDF JAR..."
./gradlew clean bootJar --no-daemon
if [ ! -f "build/libs/Stirling-PDF-"*.jar ]; then
if [ ! -f "stirling-pdf/build/libs/Stirling-PDF-"*.jar ]; then
print_error "Failed to build Stirling-PDF JAR"
exit 1
fi
# Find the built JAR
STIRLING_JAR=$(ls build/libs/Stirling-PDF-*.jar | head -n 1)
STIRLING_JAR=$(ls stirling-pdf/build/libs/Stirling-PDF-*.jar | head -n 1)
print_success "Built JAR: $STIRLING_JAR"
# Create directories for Tauri

View File

@ -8,6 +8,7 @@ use std::collections::VecDeque;
static BACKEND_PROCESS: Mutex<Option<tauri_plugin_shell::process::CommandChild>> = Mutex::new(None);
static BACKEND_LOGS: Mutex<VecDeque<String>> = Mutex::new(VecDeque::new());
static BACKEND_STARTING: Mutex<bool> = Mutex::new(false);
// Helper function to add log entry
fn add_log(message: String) {
@ -20,25 +21,42 @@ fn add_log(message: String) {
println!("{}", message); // Also print to console
}
// Helper function to reset starting flag
fn reset_starting_flag() {
let mut starting_guard = BACKEND_STARTING.lock().unwrap();
*starting_guard = false;
}
// Command to start the backend with bundled JRE
#[tauri::command]
async fn start_backend(app: tauri::AppHandle) -> Result<String, String> {
add_log("🚀 start_backend() called - Attempting to start backend with bundled JRE...".to_string());
// Check if backend is already running
// Check if backend is already running or starting
{
let process_guard = BACKEND_PROCESS.lock().unwrap();
if process_guard.is_some() {
add_log("⚠️ Backend already running, skipping start".to_string());
add_log("⚠️ Backend process already running, skipping start".to_string());
return Ok("Backend already running".to_string());
}
}
// Check and set starting flag to prevent multiple simultaneous starts
{
let mut starting_guard = BACKEND_STARTING.lock().unwrap();
if *starting_guard {
add_log("⚠️ Backend already starting, skipping duplicate start".to_string());
return Ok("Backend startup already in progress".to_string());
}
*starting_guard = true;
}
// Use Tauri's resource API to find the bundled JRE and JAR
let resource_dir = app.path().resource_dir().map_err(|e| {
let error_msg = format!("❌ Failed to get resource directory: {}", e);
add_log(error_msg.clone());
reset_starting_flag();
error_msg
})?;
@ -55,6 +73,7 @@ async fn start_backend(app: tauri::AppHandle) -> Result<String, String> {
if !java_executable.exists() {
let error_msg = format!("❌ Bundled JRE not found at: {:?}", java_executable);
add_log(error_msg.clone());
reset_starting_flag();
return Err(error_msg);
}
@ -66,26 +85,32 @@ async fn start_backend(app: tauri::AppHandle) -> Result<String, String> {
.map_err(|e| {
let error_msg = format!("Failed to read libs directory: {}. Make sure the JAR is copied to libs/", e);
add_log(error_msg.clone());
reset_starting_flag();
error_msg
})?
.filter_map(|entry| entry.ok())
.filter(|entry| {
let path = entry.path();
path.extension().and_then(|s| s.to_str()) == Some("jar")
&& path.file_name().unwrap().to_string_lossy().contains("Stirling-PDF")
// Match any .jar file containing "stirling-pdf" (case-insensitive)
path.extension().and_then(|s| s.to_str()).map(|ext| ext.eq_ignore_ascii_case("jar")).unwrap_or(false)
&& path.file_name()
.and_then(|f| f.to_str())
.map(|name| name.to_ascii_lowercase().contains("stirling-pdf"))
.unwrap_or(false)
})
.collect();
if jar_files.is_empty() {
let error_msg = "No Stirling-PDF JAR found in libs directory.".to_string();
add_log(error_msg.clone());
reset_starting_flag();
return Err(error_msg);
}
// Sort by filename to get the latest version
// Sort by filename to get the latest version (case-insensitive)
jar_files.sort_by(|a, b| {
let name_a = a.file_name().to_string_lossy().to_string();
let name_b = b.file_name().to_string_lossy().to_string();
let name_a = a.file_name().to_string_lossy().to_ascii_lowercase();
let name_b = b.file_name().to_string_lossy().to_ascii_lowercase();
name_b.cmp(&name_a) // Reverse order to get latest first
});
@ -155,6 +180,7 @@ async fn start_backend(app: tauri::AppHandle) -> Result<String, String> {
.map_err(|e| {
let error_msg = format!("❌ Failed to spawn sidecar: {}", e);
add_log(error_msg.clone());
reset_starting_flag();
error_msg
})?;
@ -249,6 +275,10 @@ async fn start_backend(app: tauri::AppHandle) -> Result<String, String> {
println!("⏳ Waiting for backend startup...");
tokio::time::sleep(std::time::Duration::from_millis(10000)).await;
// Reset the starting flag since startup is complete
reset_starting_flag();
add_log("✅ Backend startup sequence completed, starting flag cleared".to_string());
Ok("Backend startup initiated successfully with bundled JRE".to_string())
}
@ -319,8 +349,12 @@ async fn check_jar_exists(app: tauri::AppHandle) -> Result<String, String> {
.filter_map(|entry| entry.ok())
.filter(|entry| {
let path = entry.path();
path.extension().and_then(|s| s.to_str()) == Some("jar")
&& path.file_name().unwrap().to_string_lossy().contains("Stirling-PDF")
// Match any .jar file containing "stirling-pdf" (case-insensitive)
path.extension().and_then(|s| s.to_str()).map(|ext| ext.eq_ignore_ascii_case("jar")).unwrap_or(false)
&& path.file_name()
.and_then(|f| f.to_str())
.map(|name| name.to_ascii_lowercase().contains("stirling-pdf"))
.unwrap_or(false)
})
.map(|entry| entry.file_name().to_string_lossy().to_string())
.collect();

View File

@ -1,6 +1,5 @@
import React from 'react';
import { RainbowThemeProvider } from './components/shared/RainbowThemeProvider';
import React, { useEffect } from 'react';
import { RainbowThemeProvider } from './components/shared/RainbowThemeProvider';
import HomePage from './pages/HomePage';
// Import global styles
@ -15,11 +14,14 @@ export default function App() {
const initializeBackend = async () => {
try {
// Check if we're running in Tauri environment
if (typeof window !== 'undefined' && window.__TAURI__) {
if (typeof window !== 'undefined' && (window.__TAURI__ || window.__TAURI_INTERNALS__)) {
const { tauriBackendService } = await import('./services/tauriBackendService');
console.log('Running in Tauri - Starting backend on React app startup...');
await tauriBackendService.startBackend();
console.log('Backend started successfully');
}
else {
console.warn('Not running in Tauri - Backend will not be started');
}
} catch (error) {
console.error('Failed to start backend on app startup:', error);

View File

@ -1,4 +1,5 @@
import { useState, useEffect } from 'react';
import { makeApiUrl } from '../utils/api';
export interface AppConfig {
baseUrl?: string;
@ -46,7 +47,7 @@ export function useAppConfig(): UseAppConfigReturn {
setLoading(true);
setError(null);
const response = await fetch('/api/v1/config/app-config');
const response = await fetch(makeApiUrl('/api/v1/config/app-config'));
if (!response.ok) {
throw new Error(`Failed to fetch config: ${response.status} ${response.statusText}`);

View File

@ -1,9 +1,11 @@
import { useState, useEffect } from 'react';
import { makeApiUrl } from '../utils/api';
import { useBackendHealth } from './useBackendHealth';
/**
* Hook to check if a specific endpoint is enabled
*/
export function useEndpointEnabled(endpoint: string): {
export function useEndpointEnabled(endpoint: string, backendHealthy?: boolean): {
enabled: boolean | null;
loading: boolean;
error: string | null;
@ -24,7 +26,7 @@ export function useEndpointEnabled(endpoint: string): {
setLoading(true);
setError(null);
const response = await fetch(`/api/v1/config/endpoint-enabled?endpoint=${encodeURIComponent(endpoint)}`);
const response = await fetch(makeApiUrl(`/api/v1/config/endpoint-enabled?endpoint=${encodeURIComponent(endpoint)}`));
if (!response.ok) {
throw new Error(`Failed to check endpoint: ${response.status} ${response.statusText}`);
@ -42,8 +44,16 @@ export function useEndpointEnabled(endpoint: string): {
};
useEffect(() => {
fetchEndpointStatus();
}, [endpoint]);
// Only fetch endpoint status if backend is healthy (or if backendHealthy is not provided)
if (backendHealthy === undefined || backendHealthy === true) {
fetchEndpointStatus();
} else {
// Backend is not healthy, reset state
setEnabled(null);
setLoading(false);
setError('Backend not available');
}
}, [endpoint, backendHealthy]);
return {
enabled,
@ -57,7 +67,7 @@ export function useEndpointEnabled(endpoint: string): {
* Hook to check multiple endpoints at once using batch API
* Returns a map of endpoint -> enabled status
*/
export function useMultipleEndpointsEnabled(endpoints: string[]): {
export function useMultipleEndpointsEnabled(endpoints: string[], backendHealthy?: boolean): {
endpointStatus: Record<string, boolean>;
loading: boolean;
error: string | null;
@ -80,7 +90,7 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
// Use batch API for efficiency
const endpointsParam = endpoints.join(',');
const response = await fetch(`/api/v1/config/endpoints-enabled?endpoints=${encodeURIComponent(endpointsParam)}`);
const response = await fetch(makeApiUrl(`/api/v1/config/endpoints-enabled?endpoints=${encodeURIComponent(endpointsParam)}`));
if (!response.ok) {
throw new Error(`Failed to check endpoints: ${response.status} ${response.statusText}`);
@ -105,8 +115,16 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
};
useEffect(() => {
fetchAllEndpointStatuses();
}, [endpoints.join(',')]); // Re-run when endpoints array changes
// Only fetch endpoint statuses if backend is healthy (or if backendHealthy is not provided)
if (backendHealthy === undefined || backendHealthy === true) {
fetchAllEndpointStatuses();
} else {
// Backend is not healthy, reset state
setEndpointStatus({});
setLoading(false);
setError('Backend not available');
}
}, [endpoints.join(','), backendHealthy]); // Re-run when endpoints array changes or backend health changes
return {
endpointStatus,
@ -114,4 +132,50 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
error,
refetch: fetchAllEndpointStatuses,
};
}
/**
* Convenience hook that combines backend health checking with endpoint status checking
* Only checks endpoint status once backend is healthy
*/
export function useEndpointEnabledWithHealthCheck(endpoint: string): {
enabled: boolean | null;
loading: boolean;
error: string | null;
backendHealthy: boolean;
refetch: () => Promise<void>;
} {
const { isHealthy: backendHealthy } = useBackendHealth();
const { enabled, loading, error, refetch } = useEndpointEnabled(endpoint, backendHealthy);
return {
enabled,
loading,
error,
backendHealthy,
refetch,
};
}
/**
* Convenience hook that combines backend health checking with multiple endpoint status checking
* Only checks endpoint statuses once backend is healthy
*/
export function useMultipleEndpointsEnabledWithHealthCheck(endpoints: string[]): {
endpointStatus: Record<string, boolean>;
loading: boolean;
error: string | null;
backendHealthy: boolean;
refetch: () => Promise<void>;
} {
const { isHealthy: backendHealthy } = useBackendHealth();
const { endpointStatus, loading, error, refetch } = useMultipleEndpointsEnabled(endpoints, backendHealthy);
return {
endpointStatus,
loading,
error,
backendHealthy,
refetch,
};
}

View File

@ -24,7 +24,7 @@ import CompressPdfPanel from "../tools/Compress";
import MergePdfPanel from "../tools/Merge";
import ToolRenderer from "../components/tools/ToolRenderer";
import QuickAccessBar from "../components/shared/QuickAccessBar";
import { useMultipleEndpointsEnabled } from "../hooks/useEndpointConfig";
import { useMultipleEndpointsEnabledWithHealthCheck } from "../hooks/useEndpointConfig";
type ToolRegistryEntry = {
icon: React.ReactNode;
@ -79,7 +79,7 @@ export default function HomePage() {
// Get all unique endpoints for batch checking
const allEndpoints = Array.from(new Set(Object.values(toolEndpoints).flat()));
const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints);
const { endpointStatus, loading: endpointsLoading, backendHealthy } = useMultipleEndpointsEnabledWithHealthCheck(allEndpoints);
// Persist active files across reloads
useEffect(() => {

View File

@ -4,7 +4,7 @@ import { Stack, Slider, Group, Text, Button, Checkbox, TextInput, Loader, Alert
import { FileWithUrl } from "../types/file";
import { fileStorage } from "../services/fileStorage";
import { makeApiUrl } from "../utils/api";
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
import { useEndpointEnabledWithHealthCheck } from "../hooks/useEndpointConfig";
export interface CompressProps {
files?: FileWithUrl[];
@ -37,7 +37,7 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
const [selected, setSelected] = useState<boolean[]>(files.map(() => false));
const [localLoading, setLocalLoading] = useState<boolean>(false);
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("compress-pdf");
const { enabled: endpointEnabled, loading: endpointLoading, backendHealthy } = useEndpointEnabledWithHealthCheck("compress-pdf");
const {
compressionLevel,

View File

@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
import { FileWithUrl } from "../types/file";
import { fileStorage } from "../services/fileStorage";
import { makeApiUrl } from "../utils/api";
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
import { useEndpointEnabledWithHealthCheck } from "../hooks/useEndpointConfig";
export interface MergePdfPanelProps {
files: FileWithUrl[];
@ -28,7 +28,7 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
const [downloadUrl, setLocalDownloadUrl] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("merge-pdfs");
const { enabled: endpointEnabled, loading: endpointLoading, backendHealthy } = useEndpointEnabledWithHealthCheck("merge-pdfs");
useEffect(() => {
setSelectedFiles(files.map(() => true));

View File

@ -17,7 +17,7 @@ import { useTranslation } from "react-i18next";
import DownloadIcon from "@mui/icons-material/Download";
import { FileWithUrl } from "../types/file";
import { fileStorage } from "../services/fileStorage";
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
import { useEndpointEnabledWithHealthCheck } from "../hooks/useEndpointConfig";
export interface SplitPdfPanelProps {
file: { file: FileWithUrl; url: string } | null;
@ -83,7 +83,7 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
} = params;
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(getEndpointName(mode));
const { enabled: endpointEnabled, loading: endpointLoading, backendHealthy } = useEndpointEnabledWithHealthCheck(getEndpointName(mode));
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!file) {