feat: Add build scripts and runtime config support
Backend: - Add setup-backend.sh/bat for one-click backend setup - Fix test_auth.py mock settings (JWT_EXPIRE_HOURS) - Fix test_excel_export.py TEMPLATE_DIR reference Frontend: - Add config.json for runtime API URL configuration - Add init.js and settings.js for config loading - Update main.js to load config from external file - Update api.js to use dynamic API_BASE_URL - Update all pages to initialize config before API calls - Update package.json with extraResources for config Build: - Add build-client.sh/bat for packaging Electron + Sidecar - Add build-all.ps1 PowerShell script with -ApiUrl parameter - Add GitHub Actions workflow for Windows builds - Add scripts/README.md documentation This allows IT to configure backend URL without rebuilding. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
116
client/src/config/settings.js
Normal file
116
client/src/config/settings.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Runtime Configuration Module
|
||||
*
|
||||
* Loads settings from external config.json file at runtime,
|
||||
* allowing IT to configure the app without rebuilding.
|
||||
*
|
||||
* Config file location:
|
||||
* - Development: client/config.json
|
||||
* - Packaged app: <app>/resources/config.json
|
||||
*/
|
||||
|
||||
// Default settings (used if config.json is not found)
|
||||
const DEFAULT_SETTINGS = {
|
||||
apiBaseUrl: "http://localhost:8000/api",
|
||||
uploadTimeout: 600000,
|
||||
appTitle: "Meeting Assistant",
|
||||
whisper: {
|
||||
model: "medium",
|
||||
device: "cpu",
|
||||
compute: "int8"
|
||||
}
|
||||
};
|
||||
|
||||
let _settings = null;
|
||||
let _configPath = null;
|
||||
|
||||
/**
|
||||
* Get the config file path based on environment
|
||||
*/
|
||||
function getConfigPath() {
|
||||
if (_configPath) return _configPath;
|
||||
|
||||
// Check if running in Electron
|
||||
if (typeof window !== "undefined" && window.electronAPI) {
|
||||
// Will be set by preload.js
|
||||
return null;
|
||||
}
|
||||
|
||||
// Browser/development fallback
|
||||
return "./config.json";
|
||||
}
|
||||
|
||||
/**
|
||||
* Load settings from config file
|
||||
* Called once at app startup
|
||||
*/
|
||||
export async function loadSettings() {
|
||||
if (_settings) return _settings;
|
||||
|
||||
try {
|
||||
// Try to load from Electron's exposed config
|
||||
if (typeof window !== "undefined" && window.electronAPI?.getConfig) {
|
||||
_settings = await window.electronAPI.getConfig();
|
||||
console.log("Settings loaded from Electron config:", _settings);
|
||||
return _settings;
|
||||
}
|
||||
|
||||
// Fallback: try to fetch config.json
|
||||
const configPath = getConfigPath();
|
||||
if (configPath) {
|
||||
const response = await fetch(configPath);
|
||||
if (response.ok) {
|
||||
const config = await response.json();
|
||||
_settings = { ...DEFAULT_SETTINGS, ...config };
|
||||
console.log("Settings loaded from config.json:", _settings);
|
||||
return _settings;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to load config.json, using defaults:", error.message);
|
||||
}
|
||||
|
||||
// Use defaults
|
||||
_settings = { ...DEFAULT_SETTINGS };
|
||||
console.log("Using default settings:", _settings);
|
||||
return _settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current settings (must call loadSettings first)
|
||||
*/
|
||||
export function getSettings() {
|
||||
if (!_settings) {
|
||||
console.warn("Settings not loaded yet, returning defaults");
|
||||
return { ...DEFAULT_SETTINGS };
|
||||
}
|
||||
return _settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API base URL
|
||||
*/
|
||||
export function getApiBaseUrl() {
|
||||
return getSettings().apiBaseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload timeout
|
||||
*/
|
||||
export function getUploadTimeout() {
|
||||
return getSettings().uploadTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app title
|
||||
*/
|
||||
export function getAppTitle() {
|
||||
return getSettings().appTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Whisper configuration
|
||||
*/
|
||||
export function getWhisperConfig() {
|
||||
return getSettings().whisper;
|
||||
}
|
||||
@@ -8,11 +8,61 @@ let mainWindow;
|
||||
let sidecarProcess;
|
||||
let sidecarReady = false;
|
||||
let streamingActive = false;
|
||||
let appConfig = null;
|
||||
|
||||
/**
|
||||
* Load configuration from external config.json
|
||||
* Config file location:
|
||||
* - Development: client/config.json
|
||||
* - Packaged: <app>/resources/config.json
|
||||
*/
|
||||
function loadConfig() {
|
||||
const configPaths = [
|
||||
// Packaged app: resources folder
|
||||
app.isPackaged ? path.join(process.resourcesPath, "config.json") : null,
|
||||
// Development: client folder
|
||||
path.join(__dirname, "..", "config.json"),
|
||||
// Fallback: same directory
|
||||
path.join(__dirname, "config.json"),
|
||||
].filter(Boolean);
|
||||
|
||||
for (const configPath of configPaths) {
|
||||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
const configData = fs.readFileSync(configPath, "utf-8");
|
||||
appConfig = JSON.parse(configData);
|
||||
console.log("Config loaded from:", configPath);
|
||||
console.log("Config:", appConfig);
|
||||
return appConfig;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to load config from:", configPath, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Default configuration
|
||||
appConfig = {
|
||||
apiBaseUrl: "http://localhost:8000/api",
|
||||
uploadTimeout: 600000,
|
||||
appTitle: "Meeting Assistant",
|
||||
whisper: {
|
||||
model: "medium",
|
||||
device: "cpu",
|
||||
compute: "int8"
|
||||
}
|
||||
};
|
||||
console.log("Using default config:", appConfig);
|
||||
return appConfig;
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
// Set window title from config
|
||||
const windowTitle = appConfig?.appTitle || "Meeting Assistant";
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
title: windowTitle,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
@@ -32,34 +82,65 @@ function startSidecar() {
|
||||
? path.join(process.resourcesPath, "sidecar")
|
||||
: path.join(__dirname, "..", "..", "sidecar");
|
||||
|
||||
const sidecarScript = path.join(sidecarDir, "transcriber.py");
|
||||
const venvPython = path.join(sidecarDir, "venv", "bin", "python");
|
||||
// Determine the sidecar executable path based on packaging and platform
|
||||
let sidecarExecutable;
|
||||
let sidecarArgs = [];
|
||||
|
||||
if (!fs.existsSync(sidecarScript)) {
|
||||
console.log("Sidecar script not found at:", sidecarScript);
|
||||
if (app.isPackaged) {
|
||||
// Packaged app: use PyInstaller-built executable
|
||||
if (process.platform === "win32") {
|
||||
sidecarExecutable = path.join(sidecarDir, "transcriber", "transcriber.exe");
|
||||
} else if (process.platform === "darwin") {
|
||||
sidecarExecutable = path.join(sidecarDir, "transcriber", "transcriber");
|
||||
} else {
|
||||
sidecarExecutable = path.join(sidecarDir, "transcriber", "transcriber");
|
||||
}
|
||||
} else {
|
||||
// Development mode: use Python script with venv
|
||||
const sidecarScript = path.join(sidecarDir, "transcriber.py");
|
||||
|
||||
if (!fs.existsSync(sidecarScript)) {
|
||||
console.log("Sidecar script not found at:", sidecarScript);
|
||||
console.log("Transcription will not be available.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for virtual environment Python
|
||||
let venvPython;
|
||||
if (process.platform === "win32") {
|
||||
venvPython = path.join(sidecarDir, "venv", "Scripts", "python.exe");
|
||||
} else {
|
||||
venvPython = path.join(sidecarDir, "venv", "bin", "python");
|
||||
}
|
||||
|
||||
sidecarExecutable = fs.existsSync(venvPython) ? venvPython : "python3";
|
||||
sidecarArgs = [sidecarScript];
|
||||
}
|
||||
|
||||
if (!fs.existsSync(sidecarExecutable)) {
|
||||
console.log("Sidecar executable not found at:", sidecarExecutable);
|
||||
console.log("Transcription will not be available.");
|
||||
return;
|
||||
}
|
||||
|
||||
const pythonPath = fs.existsSync(venvPython) ? venvPython : "python3";
|
||||
|
||||
try {
|
||||
// Get Whisper configuration from environment variables
|
||||
// Get Whisper configuration from config.json or environment variables
|
||||
const whisperConfig = appConfig?.whisper || {};
|
||||
const whisperEnv = {
|
||||
...process.env,
|
||||
WHISPER_MODEL: process.env.WHISPER_MODEL || "medium",
|
||||
WHISPER_DEVICE: process.env.WHISPER_DEVICE || "cpu",
|
||||
WHISPER_COMPUTE: process.env.WHISPER_COMPUTE || "int8",
|
||||
WHISPER_MODEL: process.env.WHISPER_MODEL || whisperConfig.model || "medium",
|
||||
WHISPER_DEVICE: process.env.WHISPER_DEVICE || whisperConfig.device || "cpu",
|
||||
WHISPER_COMPUTE: process.env.WHISPER_COMPUTE || whisperConfig.compute || "int8",
|
||||
};
|
||||
|
||||
console.log("Starting sidecar with:", pythonPath, sidecarScript);
|
||||
console.log("Starting sidecar with:", sidecarExecutable, sidecarArgs.join(" "));
|
||||
console.log("Whisper config:", {
|
||||
model: whisperEnv.WHISPER_MODEL,
|
||||
device: whisperEnv.WHISPER_DEVICE,
|
||||
compute: whisperEnv.WHISPER_COMPUTE,
|
||||
});
|
||||
|
||||
sidecarProcess = spawn(pythonPath, [sidecarScript], {
|
||||
sidecarProcess = spawn(sidecarExecutable, sidecarArgs, {
|
||||
cwd: sidecarDir,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
env: whisperEnv,
|
||||
@@ -121,6 +202,9 @@ function startSidecar() {
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
// Load configuration first
|
||||
loadConfig();
|
||||
|
||||
createWindow();
|
||||
startSidecar();
|
||||
|
||||
@@ -144,6 +228,12 @@ app.on("window-all-closed", () => {
|
||||
});
|
||||
|
||||
// IPC handlers
|
||||
|
||||
// Get app configuration (for renderer to initialize API settings)
|
||||
ipcMain.handle("get-config", () => {
|
||||
return appConfig;
|
||||
});
|
||||
|
||||
ipcMain.handle("navigate", (event, page) => {
|
||||
mainWindow.loadFile(path.join(__dirname, "pages", `${page}.html`));
|
||||
});
|
||||
|
||||
@@ -26,8 +26,12 @@
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { initApp } from '../services/init.js';
|
||||
import { login } from '../services/api.js';
|
||||
|
||||
// Initialize app config before any API calls
|
||||
await initApp();
|
||||
|
||||
const form = document.getElementById('login-form');
|
||||
const errorAlert = document.getElementById('error-alert');
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
|
||||
@@ -235,6 +235,7 @@
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { initApp } from '../services/init.js';
|
||||
import {
|
||||
getMeeting,
|
||||
updateMeeting,
|
||||
@@ -244,6 +245,9 @@
|
||||
transcribeAudio
|
||||
} from '../services/api.js';
|
||||
|
||||
// Initialize app config before any API calls
|
||||
await initApp();
|
||||
|
||||
const meetingId = localStorage.getItem('currentMeetingId');
|
||||
let currentMeeting = null;
|
||||
let isRecording = false;
|
||||
|
||||
@@ -67,8 +67,12 @@
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { initApp } from '../services/init.js';
|
||||
import { getMeetings, createMeeting, clearToken } from '../services/api.js';
|
||||
|
||||
// Initialize app config before any API calls
|
||||
await initApp();
|
||||
|
||||
const meetingsContainer = document.getElementById('meetings-container');
|
||||
const newMeetingBtn = document.getElementById('new-meeting-btn');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
// App configuration (loaded from config.json)
|
||||
getConfig: () => ipcRenderer.invoke("get-config"),
|
||||
|
||||
// Navigation
|
||||
navigate: (page) => ipcRenderer.invoke("navigate", page),
|
||||
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000/api";
|
||||
const UPLOAD_TIMEOUT = parseInt(import.meta.env.VITE_UPLOAD_TIMEOUT || "600000", 10);
|
||||
/**
|
||||
* API Service Module
|
||||
*
|
||||
* API base URL and settings are loaded from external config at runtime,
|
||||
* allowing deployment configuration without rebuilding.
|
||||
*/
|
||||
|
||||
// Settings will be loaded from config.json via Electron IPC
|
||||
let API_BASE_URL = "http://localhost:8000/api";
|
||||
let UPLOAD_TIMEOUT = 600000;
|
||||
let _settingsLoaded = false;
|
||||
|
||||
/**
|
||||
* Initialize API settings from external config
|
||||
* Called by Electron preload or at app startup
|
||||
*/
|
||||
export async function initializeApi(settings) {
|
||||
if (settings) {
|
||||
API_BASE_URL = settings.apiBaseUrl || API_BASE_URL;
|
||||
UPLOAD_TIMEOUT = settings.uploadTimeout || UPLOAD_TIMEOUT;
|
||||
_settingsLoaded = true;
|
||||
console.log("API initialized with:", { API_BASE_URL, UPLOAD_TIMEOUT });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current API base URL
|
||||
*/
|
||||
export function getApiBaseUrl() {
|
||||
return API_BASE_URL;
|
||||
}
|
||||
|
||||
let authToken = null;
|
||||
let tokenRefreshTimer = null;
|
||||
|
||||
39
client/src/services/init.js
Normal file
39
client/src/services/init.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* App Initialization Module
|
||||
*
|
||||
* Loads configuration from Electron main process and initializes API settings.
|
||||
* Should be imported and called at the start of every page.
|
||||
*/
|
||||
|
||||
import { initializeApi } from './api.js';
|
||||
|
||||
let _initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize the app with configuration from main process
|
||||
* Call this at the start of every page
|
||||
*/
|
||||
export async function initApp() {
|
||||
if (_initialized) return;
|
||||
|
||||
try {
|
||||
// Get config from Electron main process
|
||||
if (window.electronAPI?.getConfig) {
|
||||
const config = await window.electronAPI.getConfig();
|
||||
await initializeApi(config);
|
||||
console.log("App initialized with config:", config);
|
||||
} else {
|
||||
console.warn("electronAPI not available, using defaults");
|
||||
}
|
||||
_initialized = true;
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize app:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if app is initialized
|
||||
*/
|
||||
export function isInitialized() {
|
||||
return _initialized;
|
||||
}
|
||||
Reference in New Issue
Block a user