mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-29 19:35:20 +02:00
Merge pull request #1437 from AnishSarkar22/fix/electron-nextjs
feat: improve desktop updates, branding, and local server lifecycle
This commit is contained in:
commit
5f4d62c225
23 changed files with 503 additions and 91 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
# The hosted web frontend URL. Used to intercept OAuth redirects and keep them
|
# The hosted web frontend URL. Used to intercept OAuth redirects and keep them
|
||||||
# inside the desktop app. Set to your production frontend domain.
|
# inside the desktop app. Set to your production frontend domain.
|
||||||
HOSTED_FRONTEND_URL=https://surfsense.net
|
HOSTED_FRONTEND_URL=https://surfsense.com
|
||||||
|
|
||||||
# Runtime override for the above (read at app start, no rebuild required).
|
# Runtime override for the above (read at app start, no rebuild required).
|
||||||
# Useful for self-hosters whose backend NEXT_FRONTEND_URL differs from the
|
# Useful for self-hosters whose backend NEXT_FRONTEND_URL differs from the
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "surfsense-desktop",
|
"name": "surfsense-desktop",
|
||||||
|
"productName": "SurfSense",
|
||||||
"version": "0.0.25",
|
"version": "0.0.25",
|
||||||
"description": "SurfSense Desktop App",
|
"description": "SurfSense Desktop App",
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ export const IPC_CHANNELS = {
|
||||||
OPEN_EXTERNAL: 'open-external',
|
OPEN_EXTERNAL: 'open-external',
|
||||||
GET_APP_VERSION: 'get-app-version',
|
GET_APP_VERSION: 'get-app-version',
|
||||||
DEEP_LINK: 'deep-link',
|
DEEP_LINK: 'deep-link',
|
||||||
|
UPDATE_DOWNLOADED: 'update:downloaded',
|
||||||
|
UPDATE_INSTALL_NOW: 'update:install-now',
|
||||||
QUICK_ASK_TEXT: 'quick-ask-text',
|
QUICK_ASK_TEXT: 'quick-ask-text',
|
||||||
SET_QUICK_ASK_MODE: 'set-quick-ask-mode',
|
SET_QUICK_ASK_MODE: 'set-quick-ask-mode',
|
||||||
GET_QUICK_ASK_MODE: 'get-quick-ask-mode',
|
GET_QUICK_ASK_MODE: 'get-quick-ask-mode',
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ import {
|
||||||
stopAgentFilesystemTreeWatch,
|
stopAgentFilesystemTreeWatch,
|
||||||
type AgentFilesystemTreeWatchOptions,
|
type AgentFilesystemTreeWatchOptions,
|
||||||
} from '../modules/agent-filesystem-tree-watcher';
|
} from '../modules/agent-filesystem-tree-watcher';
|
||||||
|
import { installDownloadedUpdate } from '../modules/auto-updater';
|
||||||
|
|
||||||
let authTokens: { bearer: string; refresh: string } | null = null;
|
let authTokens: { bearer: string; refresh: string } | null = null;
|
||||||
|
|
||||||
|
|
@ -70,6 +71,10 @@ export function registerIpcHandlers(): void {
|
||||||
return app.getVersion();
|
return app.getVersion();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.UPDATE_INSTALL_NOW, () => {
|
||||||
|
installDownloadedUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.GET_PERMISSIONS_STATUS, () => {
|
ipcMain.handle(IPC_CHANNELS.GET_PERMISSIONS_STATUS, () => {
|
||||||
return getPermissionsStatus();
|
return getPermissionsStatus();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
|
|
||||||
import { registerGlobalErrorHandlers, showErrorDialog } from './modules/errors';
|
import { registerGlobalErrorHandlers, showErrorDialog } from './modules/errors';
|
||||||
import { startNextServer } from './modules/server';
|
import { startNextServer, stopNextServer } from './modules/server';
|
||||||
import { createMainWindow, getMainWindow, markQuitting } from './modules/window';
|
import { createMainWindow, getMainWindow, markQuitting } from './modules/window';
|
||||||
import { setupDeepLinks, handlePendingDeepLink, hasPendingDeepLink } from './modules/deep-links';
|
import { setupDeepLinks, handlePendingDeepLink, hasPendingDeepLink } from './modules/deep-links';
|
||||||
import { setupAutoUpdater } from './modules/auto-updater';
|
import { setupAutoUpdater } from './modules/auto-updater';
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
} from './modules/auto-launch';
|
} from './modules/auto-launch';
|
||||||
|
|
||||||
registerGlobalErrorHandlers();
|
registerGlobalErrorHandlers();
|
||||||
|
app.setName('SurfSense');
|
||||||
|
|
||||||
if (!setupDeepLinks()) {
|
if (!setupDeepLinks()) {
|
||||||
app.quit();
|
app.quit();
|
||||||
|
|
@ -93,6 +94,7 @@ app.on('will-quit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
unregisterQuickAsk();
|
unregisterQuickAsk();
|
||||||
unregisterFolderWatcher();
|
unregisterFolderWatcher();
|
||||||
|
stopNextServer();
|
||||||
destroyTray();
|
destroyTray();
|
||||||
await shutdownAnalytics();
|
await shutdownAnalytics();
|
||||||
app.exit();
|
app.exit();
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,201 @@
|
||||||
import { app, dialog } from 'electron';
|
import { app, BrowserWindow, dialog } from 'electron';
|
||||||
|
import { IPC_CHANNELS } from '../ipc/channels';
|
||||||
import { trackEvent } from './analytics';
|
import { trackEvent } from './analytics';
|
||||||
|
|
||||||
const SEMVER_RE = /^\d+\.\d+\.\d+/;
|
const SEMVER_RE = /^\d+\.\d+\.\d+/;
|
||||||
|
|
||||||
export function setupAutoUpdater(): void {
|
type AutoUpdater = {
|
||||||
if (!app.isPackaged) return;
|
autoDownload: boolean;
|
||||||
|
on(event: string, listener: (...args: any[]) => void): void;
|
||||||
|
once(event: string, listener: (...args: any[]) => void): void;
|
||||||
|
removeListener(event: string, listener: (...args: any[]) => void): void;
|
||||||
|
checkForUpdates(): Promise<unknown>;
|
||||||
|
quitAndInstall(): void;
|
||||||
|
};
|
||||||
|
|
||||||
const version = app.getVersion();
|
type UpdateInfo = {
|
||||||
if (!SEMVER_RE.test(version)) {
|
version: string;
|
||||||
console.log(`Auto-updater: skipping — "${version}" is not valid semver`);
|
};
|
||||||
return;
|
|
||||||
|
type UpdateMenuState =
|
||||||
|
| { status: 'idle' }
|
||||||
|
| { status: 'downloading'; version: string }
|
||||||
|
| { status: 'ready'; version: string };
|
||||||
|
|
||||||
|
let listenersRegistered = false;
|
||||||
|
let updateMenuState: UpdateMenuState = { status: 'idle' };
|
||||||
|
const updateMenuStateListeners = new Set<(state: UpdateMenuState) => void>();
|
||||||
|
|
||||||
|
export function getUpdateMenuState(): UpdateMenuState {
|
||||||
|
return updateMenuState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onUpdateMenuStateChange(listener: (state: UpdateMenuState) => void): () => void {
|
||||||
|
updateMenuStateListeners.add(listener);
|
||||||
|
return () => {
|
||||||
|
updateMenuStateListeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUpdateMenuState(state: UpdateMenuState): void {
|
||||||
|
updateMenuState = state;
|
||||||
|
for (const listener of updateMenuStateListeners) {
|
||||||
|
listener(state);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAutoUpdater(): AutoUpdater {
|
||||||
const { autoUpdater } = require('electron-updater');
|
const { autoUpdater } = require('electron-updater');
|
||||||
|
return autoUpdater as AutoUpdater;
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureAutoUpdater(autoUpdater: AutoUpdater): void {
|
||||||
autoUpdater.autoDownload = true;
|
autoUpdater.autoDownload = true;
|
||||||
|
|
||||||
autoUpdater.on('update-available', (info: { version: string }) => {
|
if (listenersRegistered) return;
|
||||||
|
listenersRegistered = true;
|
||||||
|
|
||||||
|
const version = app.getVersion();
|
||||||
|
|
||||||
|
autoUpdater.on('update-available', (info: UpdateInfo) => {
|
||||||
console.log(`Update available: ${info.version}`);
|
console.log(`Update available: ${info.version}`);
|
||||||
|
setUpdateMenuState({ status: 'downloading', version: info.version });
|
||||||
trackEvent('desktop_update_available', {
|
trackEvent('desktop_update_available', {
|
||||||
current_version: version,
|
current_version: version,
|
||||||
new_version: info.version,
|
new_version: info.version,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on('update-downloaded', (info: { version: string }) => {
|
autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
|
||||||
console.log(`Update downloaded: ${info.version}`);
|
console.log(`Update downloaded: ${info.version}`);
|
||||||
|
setUpdateMenuState({ status: 'ready', version: info.version });
|
||||||
trackEvent('desktop_update_downloaded', {
|
trackEvent('desktop_update_downloaded', {
|
||||||
current_version: version,
|
current_version: version,
|
||||||
new_version: info.version,
|
new_version: info.version,
|
||||||
});
|
});
|
||||||
dialog.showMessageBox({
|
notifyRenderersUpdateDownloaded(info);
|
||||||
type: 'info',
|
});
|
||||||
buttons: ['Restart', 'Later'],
|
|
||||||
defaultId: 0,
|
autoUpdater.on('update-not-available', () => {
|
||||||
title: 'Update Ready',
|
setUpdateMenuState({ status: 'idle' });
|
||||||
message: `Version ${info.version} has been downloaded. Restart to apply the update.`,
|
|
||||||
}).then(({ response }: { response: number }) => {
|
|
||||||
if (response === 0) {
|
|
||||||
trackEvent('desktop_update_install_accepted', { new_version: info.version });
|
|
||||||
autoUpdater.quitAndInstall();
|
|
||||||
} else {
|
|
||||||
trackEvent('desktop_update_install_deferred', { new_version: info.version });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on('error', (err: Error) => {
|
autoUpdater.on('error', (err: Error) => {
|
||||||
|
setUpdateMenuState({ status: 'idle' });
|
||||||
console.log('Auto-updater: update check skipped —', err.message?.split('\n')[0]);
|
console.log('Auto-updater: update check skipped —', err.message?.split('\n')[0]);
|
||||||
trackEvent('desktop_update_error', {
|
trackEvent('desktop_update_error', {
|
||||||
message: err.message?.split('\n')[0],
|
message: err.message?.split('\n')[0],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyRenderersUpdateDownloaded(info: UpdateInfo): void {
|
||||||
|
for (const win of BrowserWindow.getAllWindows()) {
|
||||||
|
if (!win.isDestroyed()) {
|
||||||
|
win.webContents.send(IPC_CHANNELS.UPDATE_DOWNLOADED, {
|
||||||
|
version: info.version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installDownloadedUpdate(): void {
|
||||||
|
const autoUpdater = getAutoUpdater();
|
||||||
|
trackEvent('desktop_update_install_accepted', { source: 'renderer_prompt' });
|
||||||
|
autoUpdater.quitAndInstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupAutoUpdater(): void {
|
||||||
|
if (!app.isPackaged) return;
|
||||||
|
|
||||||
|
const version = app.getVersion();
|
||||||
|
if (!SEMVER_RE.test(version)) {
|
||||||
|
console.log(`Auto-updater: skipping - "${version}" is not valid semver`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoUpdater = getAutoUpdater();
|
||||||
|
configureAutoUpdater(autoUpdater);
|
||||||
|
|
||||||
autoUpdater.checkForUpdates().catch(() => {});
|
autoUpdater.checkForUpdates().catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function checkForUpdatesManually(): Promise<void> {
|
||||||
|
const currentState = getUpdateMenuState();
|
||||||
|
if (currentState.status === 'ready') {
|
||||||
|
installDownloadedUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentState.status === 'downloading') return;
|
||||||
|
|
||||||
|
if (!app.isPackaged) {
|
||||||
|
await dialog.showMessageBox({
|
||||||
|
type: 'info',
|
||||||
|
title: 'Updates Unavailable',
|
||||||
|
message: 'Updates are only available in packaged builds.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = app.getVersion();
|
||||||
|
if (!SEMVER_RE.test(version)) {
|
||||||
|
await dialog.showMessageBox({
|
||||||
|
type: 'info',
|
||||||
|
title: 'Updates Unavailable',
|
||||||
|
message: `Version "${version}" is not a valid release version, so updates cannot be checked.`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoUpdater = getAutoUpdater();
|
||||||
|
configureAutoUpdater(autoUpdater);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await new Promise<'not-available' | 'downloaded'>((resolve, reject) => {
|
||||||
|
const cleanup = () => {
|
||||||
|
autoUpdater.removeListener('update-available', onAvailable);
|
||||||
|
autoUpdater.removeListener('update-not-available', onNotAvailable);
|
||||||
|
autoUpdater.removeListener('update-downloaded', onDownloaded);
|
||||||
|
autoUpdater.removeListener('error', onError);
|
||||||
|
};
|
||||||
|
const onAvailable = () => {};
|
||||||
|
const onNotAvailable = () => {
|
||||||
|
cleanup();
|
||||||
|
resolve('not-available');
|
||||||
|
};
|
||||||
|
const onDownloaded = () => {
|
||||||
|
cleanup();
|
||||||
|
resolve('downloaded');
|
||||||
|
};
|
||||||
|
const onError = (err: Error) => {
|
||||||
|
cleanup();
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
autoUpdater.once('update-available', onAvailable);
|
||||||
|
autoUpdater.once('update-not-available', onNotAvailable);
|
||||||
|
autoUpdater.once('update-downloaded', onDownloaded);
|
||||||
|
autoUpdater.once('error', onError);
|
||||||
|
autoUpdater.checkForUpdates().catch((err: Error) => {
|
||||||
|
cleanup();
|
||||||
|
setUpdateMenuState({ status: 'idle' });
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result === 'not-available') {
|
||||||
|
await dialog.showMessageBox({
|
||||||
|
type: 'info',
|
||||||
|
title: 'No Updates Available',
|
||||||
|
message: "You're up to date.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setUpdateMenuState({ status: 'idle' });
|
||||||
|
await dialog.showMessageBox({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Update Check Failed',
|
||||||
|
message: err instanceof Error ? err.message : String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getMainWindow } from './window';
|
import { getMainWindow } from './window';
|
||||||
import { getServerPort } from './server';
|
import { getServerOrigin } from './server';
|
||||||
import { trackEvent } from './analytics';
|
import { trackEvent } from './analytics';
|
||||||
|
|
||||||
const PROTOCOL = 'surfsense';
|
const PROTOCOL = 'surfsense';
|
||||||
|
|
@ -23,7 +23,7 @@ function handleDeepLink(url: string) {
|
||||||
});
|
});
|
||||||
if (parsed.hostname === 'auth' && parsed.pathname === '/callback') {
|
if (parsed.hostname === 'auth' && parsed.pathname === '/callback') {
|
||||||
const params = parsed.searchParams.toString();
|
const params = parsed.searchParams.toString();
|
||||||
win.loadURL(`http://localhost:${getServerPort()}/auth/callback?${params}`);
|
win.loadURL(`${getServerOrigin()}/auth/callback?${params}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
win.show();
|
win.show();
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,118 @@
|
||||||
import { Menu } from 'electron';
|
import { app, Menu, shell } from 'electron';
|
||||||
|
import {
|
||||||
|
checkForUpdatesManually,
|
||||||
|
getUpdateMenuState,
|
||||||
|
installDownloadedUpdate,
|
||||||
|
onUpdateMenuStateChange,
|
||||||
|
} from './auto-updater';
|
||||||
|
|
||||||
|
let updateMenuListenerRegistered = false;
|
||||||
|
|
||||||
|
function getUpdateMenuItem(): Electron.MenuItemConstructorOptions {
|
||||||
|
const state = getUpdateMenuState();
|
||||||
|
|
||||||
|
if (state.status === 'downloading') {
|
||||||
|
return {
|
||||||
|
label: 'Downloading...',
|
||||||
|
enabled: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status === 'ready') {
|
||||||
|
return {
|
||||||
|
label: 'Install and Restart',
|
||||||
|
click: () => {
|
||||||
|
installDownloadedUpdate();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: 'Check for Updates...',
|
||||||
|
click: () => {
|
||||||
|
void checkForUpdatesManually();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const privacyPolicyItem: Electron.MenuItemConstructorOptions = {
|
||||||
|
label: 'Privacy Policy',
|
||||||
|
click: () => {
|
||||||
|
void shell.openExternal('https://www.surfsense.com/privacy');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const termsOfServiceItem: Electron.MenuItemConstructorOptions = {
|
||||||
|
label: 'Terms of Service',
|
||||||
|
click: () => {
|
||||||
|
void shell.openExternal('https://www.surfsense.com/terms');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export function setupMenu(): void {
|
export function setupMenu(): void {
|
||||||
|
if (!updateMenuListenerRegistered) {
|
||||||
|
updateMenuListenerRegistered = true;
|
||||||
|
onUpdateMenuStateChange(() => {
|
||||||
|
setupMenu();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const isMac = process.platform === 'darwin';
|
const isMac = process.platform === 'darwin';
|
||||||
|
const isDev = !app.isPackaged;
|
||||||
|
const updateMenuItem = getUpdateMenuItem();
|
||||||
|
const viewSubmenu: Electron.MenuItemConstructorOptions[] = [
|
||||||
|
{ role: 'reload' as const },
|
||||||
|
{ role: 'forceReload' as const },
|
||||||
|
...(isDev
|
||||||
|
? [
|
||||||
|
{ role: 'toggleDevTools' as const },
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{ type: 'separator' as const },
|
||||||
|
{ role: 'resetZoom' as const },
|
||||||
|
{ role: 'zoomIn' as const },
|
||||||
|
{ role: 'zoomOut' as const },
|
||||||
|
{ type: 'separator' as const },
|
||||||
|
{ role: 'togglefullscreen' as const },
|
||||||
|
];
|
||||||
const template: Electron.MenuItemConstructorOptions[] = [
|
const template: Electron.MenuItemConstructorOptions[] = [
|
||||||
...(isMac ? [{ role: 'appMenu' as const }] : []),
|
...(isMac
|
||||||
|
? [{
|
||||||
|
label: app.name,
|
||||||
|
submenu: [
|
||||||
|
{ role: 'about' as const },
|
||||||
|
updateMenuItem,
|
||||||
|
{ type: 'separator' as const },
|
||||||
|
{ role: 'services' as const },
|
||||||
|
{ type: 'separator' as const },
|
||||||
|
{ role: 'hide' as const },
|
||||||
|
{ role: 'hideOthers' as const },
|
||||||
|
{ role: 'unhide' as const },
|
||||||
|
{ type: 'separator' as const },
|
||||||
|
{ role: 'quit' as const },
|
||||||
|
],
|
||||||
|
}]
|
||||||
|
: []),
|
||||||
{ role: 'fileMenu' as const },
|
{ role: 'fileMenu' as const },
|
||||||
{ role: 'editMenu' as const },
|
{ role: 'editMenu' as const },
|
||||||
{ role: 'viewMenu' as const },
|
{
|
||||||
|
label: 'View',
|
||||||
|
submenu: viewSubmenu,
|
||||||
|
},
|
||||||
{ role: 'windowMenu' as const },
|
{ role: 'windowMenu' as const },
|
||||||
|
{
|
||||||
|
role: 'help' as const,
|
||||||
|
submenu: [
|
||||||
|
...(!isMac
|
||||||
|
? [
|
||||||
|
updateMenuItem,
|
||||||
|
{ type: 'separator' as const },
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
privacyPolicyItem,
|
||||||
|
termsOfServiceItem,
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { BrowserWindow, clipboard, globalShortcut, ipcMain, screen, shell } from 'electron';
|
import { app, BrowserWindow, clipboard, globalShortcut, ipcMain, screen, shell } from 'electron';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { IPC_CHANNELS } from '../ipc/channels';
|
import { IPC_CHANNELS } from '../ipc/channels';
|
||||||
import { checkAccessibilityPermission, getFrontmostApp, simulateCopy, simulatePaste } from './platform';
|
import { checkAccessibilityPermission, getFrontmostApp, simulateCopy, simulatePaste } from './platform';
|
||||||
import { getServerPort } from './server';
|
import { getServerOrigin } from './server';
|
||||||
import { getShortcuts } from './shortcuts';
|
import { getShortcuts } from './shortcuts';
|
||||||
import { getActiveSearchSpaceId } from './active-search-space';
|
import { getActiveSearchSpaceId } from './active-search-space';
|
||||||
import { trackEvent } from './analytics';
|
import { trackEvent } from './analytics';
|
||||||
|
|
@ -51,6 +51,7 @@ function createQuickAskWindow(x: number, y: number): BrowserWindow {
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
sandbox: true,
|
sandbox: true,
|
||||||
|
devTools: !app.isPackaged,
|
||||||
},
|
},
|
||||||
show: false,
|
show: false,
|
||||||
skipTaskbar: true,
|
skipTaskbar: true,
|
||||||
|
|
@ -58,7 +59,7 @@ function createQuickAskWindow(x: number, y: number): BrowserWindow {
|
||||||
|
|
||||||
const spaceId = pendingSearchSpaceId;
|
const spaceId = pendingSearchSpaceId;
|
||||||
const route = spaceId ? `/dashboard/${spaceId}/new-chat` : '/dashboard';
|
const route = spaceId ? `/dashboard/${spaceId}/new-chat` : '/dashboard';
|
||||||
quickAskWindow.loadURL(`http://localhost:${getServerPort()}${route}?quickAssist=true`);
|
quickAskWindow.loadURL(`${getServerOrigin()}${route}?quickAssist=true`);
|
||||||
|
|
||||||
quickAskWindow.once('ready-to-show', () => {
|
quickAskWindow.once('ready-to-show', () => {
|
||||||
quickAskWindow?.show();
|
quickAskWindow?.show();
|
||||||
|
|
@ -69,7 +70,7 @@ function createQuickAskWindow(x: number, y: number): BrowserWindow {
|
||||||
});
|
});
|
||||||
|
|
||||||
quickAskWindow.webContents.setWindowOpenHandler(({ url }) => {
|
quickAskWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
if (url.startsWith('http://localhost')) {
|
if (url.startsWith(getServerOrigin())) {
|
||||||
return { action: 'allow' };
|
return { action: 'allow' };
|
||||||
}
|
}
|
||||||
shell.openExternal(url);
|
shell.openExternal(url);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { BrowserWindow, desktopCapturer, nativeImage, screen } from 'electron';
|
import { app, BrowserWindow, desktopCapturer, nativeImage, screen } from 'electron';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { IPC_CHANNELS } from '../../ipc/channels';
|
import { IPC_CHANNELS } from '../../ipc/channels';
|
||||||
function fitNativeImageToWorkArea(img: Electron.NativeImage, display: Electron.Display): Electron.NativeImage {
|
function fitNativeImageToWorkArea(img: Electron.NativeImage, display: Electron.Display): Electron.NativeImage {
|
||||||
|
|
@ -261,6 +261,7 @@ export function pickScreenRegion(opts?: { windowDataUrl?: string }): Promise<str
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
sandbox: true,
|
sandbox: true,
|
||||||
|
devTools: !app.isPackaged,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { BrowserWindow, desktopCapturer, ipcMain, screen } from 'electron';
|
import { app, BrowserWindow, desktopCapturer, ipcMain, screen } from 'electron';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { IPC_CHANNELS } from '../../ipc/channels';
|
import { IPC_CHANNELS } from '../../ipc/channels';
|
||||||
|
|
||||||
|
|
@ -185,6 +185,7 @@ export function pickOpenWindowCapture(): Promise<PickedWindowResult | null> {
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
sandbox: true,
|
sandbox: true,
|
||||||
|
devTools: !app.isPackaged,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,20 @@
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { app } from 'electron';
|
import { app, utilityProcess } from 'electron';
|
||||||
import { getPort } from 'get-port-please';
|
import { getPort } from 'get-port-please';
|
||||||
|
|
||||||
const isDev = !app.isPackaged;
|
const isDev = !app.isPackaged;
|
||||||
|
const SERVER_HOST = '127.0.0.1';
|
||||||
let serverPort = 3000;
|
let serverPort = 3000;
|
||||||
|
let nextServerProcess: ReturnType<typeof utilityProcess.fork> | null = null;
|
||||||
|
|
||||||
export function getServerPort(): number {
|
export function getServerPort(): number {
|
||||||
return serverPort;
|
return serverPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getServerOrigin(): string {
|
||||||
|
return `http://${SERVER_HOST}:${serverPort}`;
|
||||||
|
}
|
||||||
|
|
||||||
function getStandalonePath(): string {
|
function getStandalonePath(): string {
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
return path.join(__dirname, '..', '..', 'surfsense_web', '.next', 'standalone', 'surfsense_web');
|
return path.join(__dirname, '..', '..', 'surfsense_web', '.next', 'standalone', 'surfsense_web');
|
||||||
|
|
@ -38,17 +44,55 @@ export async function startNextServer(): Promise<void> {
|
||||||
const standalonePath = getStandalonePath();
|
const standalonePath = getStandalonePath();
|
||||||
const serverScript = path.join(standalonePath, 'server.js');
|
const serverScript = path.join(standalonePath, 'server.js');
|
||||||
|
|
||||||
process.env.PORT = String(serverPort);
|
const child = utilityProcess.fork(serverScript, [], {
|
||||||
// Loopback bind: 0.0.0.0 leaks into request.url and flips window origin via NextResponse.redirect.
|
cwd: standalonePath,
|
||||||
process.env.HOSTNAME = 'localhost';
|
env: {
|
||||||
process.env.NODE_ENV = 'production';
|
...process.env,
|
||||||
process.chdir(standalonePath);
|
PORT: String(serverPort),
|
||||||
|
// Loopback bind: avoids 0.0.0.0 leaking into request.url and redirect origins.
|
||||||
|
HOSTNAME: SERVER_HOST,
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
},
|
||||||
|
serviceName: 'SurfSense Next Server',
|
||||||
|
stdio: 'pipe',
|
||||||
|
});
|
||||||
|
nextServerProcess = child;
|
||||||
|
|
||||||
require(serverScript);
|
child.stdout?.on('data', (chunk) => {
|
||||||
|
process.stdout.write(chunk);
|
||||||
|
});
|
||||||
|
child.stderr?.on('data', (chunk) => {
|
||||||
|
process.stderr.write(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
const ready = await waitForServer(`http://localhost:${serverPort}`);
|
const handleExit = (code: number) => {
|
||||||
|
if (nextServerProcess === child) {
|
||||||
|
nextServerProcess = null;
|
||||||
|
}
|
||||||
|
console.error(`Next.js server exited with code ${code}`);
|
||||||
|
};
|
||||||
|
child.on('exit', handleExit);
|
||||||
|
|
||||||
|
let startupExitHandler: ((code: number) => void) | null = null;
|
||||||
|
const exited = new Promise<never>((_resolve, reject) => {
|
||||||
|
startupExitHandler = (code: number) => {
|
||||||
|
reject(new Error(`Next.js server exited before startup completed with code ${code}`));
|
||||||
|
};
|
||||||
|
child.once('exit', startupExitHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ready = await Promise.race([waitForServer(getServerOrigin()), exited]);
|
||||||
|
if (startupExitHandler) {
|
||||||
|
child.removeListener('exit', startupExitHandler);
|
||||||
|
}
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
|
stopNextServer();
|
||||||
throw new Error('Next.js server failed to start within 30 s');
|
throw new Error('Next.js server failed to start within 30 s');
|
||||||
}
|
}
|
||||||
console.log(`Next.js server ready on port ${serverPort}`);
|
console.log(`Next.js server ready on port ${serverPort}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function stopNextServer(): void {
|
||||||
|
nextServerProcess?.kill();
|
||||||
|
nextServerProcess = null;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,30 @@ let tray: Tray | null = null;
|
||||||
let registeredGeneralAssist: string | null = null;
|
let registeredGeneralAssist: string | null = null;
|
||||||
let registeredScreenshotAssist: string | null = null;
|
let registeredScreenshotAssist: string | null = null;
|
||||||
|
|
||||||
|
function buildContextMenu(screenshotAccelerator: string): Menu {
|
||||||
|
return Menu.buildFromTemplate([
|
||||||
|
{ label: 'Open SurfSense', click: () => showMainWindow('tray_menu') },
|
||||||
|
{
|
||||||
|
label: 'Take Screenshot\u2026',
|
||||||
|
accelerator: screenshotAccelerator || undefined,
|
||||||
|
click: () => {
|
||||||
|
trackEvent('desktop_tray_screenshot_clicked');
|
||||||
|
void Promise.resolve(runScreenshotAssistShortcut()).catch((err) => {
|
||||||
|
console.error('[tray] Screenshot Assist failed:', err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Quit',
|
||||||
|
click: () => {
|
||||||
|
trackEvent('desktop_tray_quit_clicked');
|
||||||
|
app.exit(0);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
function getTrayIcon(): NativeImage {
|
function getTrayIcon(): NativeImage {
|
||||||
const iconName =
|
const iconName =
|
||||||
process.platform === 'darwin'
|
process.platform === 'darwin'
|
||||||
|
|
@ -59,22 +83,10 @@ export async function createTray(): Promise<void> {
|
||||||
tray = new Tray(getTrayIcon());
|
tray = new Tray(getTrayIcon());
|
||||||
tray.setToolTip('SurfSense');
|
tray.setToolTip('SurfSense');
|
||||||
|
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
const shortcuts = await getShortcuts();
|
||||||
{ label: 'Open SurfSense', click: () => showMainWindow('tray_menu') },
|
tray.setContextMenu(buildContextMenu(shortcuts.screenshotAssist));
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: 'Quit',
|
|
||||||
click: () => {
|
|
||||||
trackEvent('desktop_tray_quit_clicked');
|
|
||||||
app.exit(0);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
tray.setContextMenu(contextMenu);
|
|
||||||
tray.on('double-click', () => showMainWindow('tray_click'));
|
tray.on('double-click', () => showMainWindow('tray_click'));
|
||||||
|
|
||||||
const shortcuts = await getShortcuts();
|
|
||||||
registeredGeneralAssist = registerOne(
|
registeredGeneralAssist = registerOne(
|
||||||
null,
|
null,
|
||||||
shortcuts.generalAssist,
|
shortcuts.generalAssist,
|
||||||
|
|
@ -107,6 +119,7 @@ export async function reregisterScreenshotAssist(): Promise<void> {
|
||||||
runScreenshotAssistShortcut,
|
runScreenshotAssistShortcut,
|
||||||
'Screenshot Assist'
|
'Screenshot Assist'
|
||||||
);
|
);
|
||||||
|
tray?.setContextMenu(buildContextMenu(shortcuts.screenshotAssist));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function destroyTray(): void {
|
export function destroyTray(): void {
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,18 @@ import { app, BrowserWindow, shell, session } from 'electron';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { trackEvent } from './analytics';
|
import { trackEvent } from './analytics';
|
||||||
import { showErrorDialog } from './errors';
|
import { showErrorDialog } from './errors';
|
||||||
import { getServerPort } from './server';
|
import { getServerOrigin, getServerPort } from './server';
|
||||||
import { setActiveSearchSpaceId } from './active-search-space';
|
import { setActiveSearchSpaceId } from './active-search-space';
|
||||||
|
|
||||||
const isDev = !app.isPackaged;
|
const isDev = !app.isPackaged;
|
||||||
const isMac = process.platform === 'darwin';
|
const isMac = process.platform === 'darwin';
|
||||||
|
const WINDOW_TITLE = 'SurfSense';
|
||||||
|
|
||||||
function getHostedFrontendUrl(): string {
|
function getHostedFrontendUrl(): string {
|
||||||
return (
|
return (
|
||||||
process.env.SURFSENSE_HOSTED_FRONTEND_URL_OVERRIDE ||
|
process.env.SURFSENSE_HOSTED_FRONTEND_URL_OVERRIDE ||
|
||||||
process.env.HOSTED_FRONTEND_URL ||
|
process.env.HOSTED_FRONTEND_URL ||
|
||||||
'https://surfsense.net'
|
'https://surfsense.com'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,6 +42,7 @@ export function markQuitting(): void {
|
||||||
|
|
||||||
export function createMainWindow(initialPath = '/dashboard'): BrowserWindow {
|
export function createMainWindow(initialPath = '/dashboard'): BrowserWindow {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
|
title: WINDOW_TITLE,
|
||||||
width: 1280,
|
width: 1280,
|
||||||
height: 800,
|
height: 800,
|
||||||
minWidth: 800,
|
minWidth: 800,
|
||||||
|
|
@ -51,6 +53,7 @@ export function createMainWindow(initialPath = '/dashboard'): BrowserWindow {
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
sandbox: true,
|
sandbox: true,
|
||||||
webviewTag: false,
|
webviewTag: false,
|
||||||
|
devTools: !app.isPackaged,
|
||||||
},
|
},
|
||||||
show: false,
|
show: false,
|
||||||
...(isMac
|
...(isMac
|
||||||
|
|
@ -65,10 +68,18 @@ export function createMainWindow(initialPath = '/dashboard'): BrowserWindow {
|
||||||
mainWindow?.show();
|
mainWindow?.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.loadURL(`http://localhost:${getServerPort()}${initialPath}`);
|
mainWindow.webContents.on('page-title-updated', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
mainWindow?.setTitle(WINDOW_TITLE);
|
||||||
|
});
|
||||||
|
mainWindow.webContents.on('did-finish-load', () => {
|
||||||
|
mainWindow?.setTitle(WINDOW_TITLE);
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.loadURL(`${getServerOrigin()}${initialPath}`);
|
||||||
|
|
||||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
if (url.startsWith('http://localhost')) {
|
if (url.startsWith(getServerOrigin())) {
|
||||||
return { action: 'allow' };
|
return { action: 'allow' };
|
||||||
}
|
}
|
||||||
shell.openExternal(url);
|
shell.openExternal(url);
|
||||||
|
|
@ -84,8 +95,9 @@ export function createMainWindow(initialPath = '/dashboard'): BrowserWindow {
|
||||||
try {
|
try {
|
||||||
const u = new URL(details.url);
|
const u = new URL(details.url);
|
||||||
const originalHost = u.host;
|
const originalHost = u.host;
|
||||||
u.protocol = 'http:';
|
const local = new URL(getServerOrigin());
|
||||||
u.host = `localhost:${getServerPort()}`;
|
u.protocol = local.protocol;
|
||||||
|
u.host = local.host;
|
||||||
trackEvent('desktop_oauth_redirect_intercepted', {
|
trackEvent('desktop_oauth_redirect_intercepted', {
|
||||||
host: originalHost,
|
host: originalHost,
|
||||||
path: u.pathname,
|
path: u.pathname,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
},
|
},
|
||||||
openExternal: (url: string) => ipcRenderer.send(IPC_CHANNELS.OPEN_EXTERNAL, url),
|
openExternal: (url: string) => ipcRenderer.send(IPC_CHANNELS.OPEN_EXTERNAL, url),
|
||||||
getAppVersion: () => ipcRenderer.invoke(IPC_CHANNELS.GET_APP_VERSION),
|
getAppVersion: () => ipcRenderer.invoke(IPC_CHANNELS.GET_APP_VERSION),
|
||||||
|
onUpdateDownloaded: (callback: (data: { version: string }) => void) => {
|
||||||
|
const listener = (_event: unknown, data: { version: string }) => callback(data);
|
||||||
|
ipcRenderer.on(IPC_CHANNELS.UPDATE_DOWNLOADED, listener);
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener(IPC_CHANNELS.UPDATE_DOWNLOADED, listener);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
installUpdateNow: () => ipcRenderer.invoke(IPC_CHANNELS.UPDATE_INSTALL_NOW),
|
||||||
onDeepLink: (callback: (url: string) => void) => {
|
onDeepLink: (callback: (url: string) => void) => {
|
||||||
const listener = (_event: unknown, url: string) => callback(url);
|
const listener = (_event: unknown, url: string) => callback(url);
|
||||||
ipcRenderer.on(IPC_CHANNELS.DEEP_LINK, listener);
|
ipcRenderer.on(IPC_CHANNELS.DEEP_LINK, listener);
|
||||||
|
|
|
||||||
|
|
@ -474,8 +474,10 @@ export function AgentPermissionsContent() {
|
||||||
handleConfirmDelete();
|
handleConfirmDelete();
|
||||||
}}
|
}}
|
||||||
disabled={deleteMutation.isPending}
|
disabled={deleteMutation.isPending}
|
||||||
|
className="relative min-w-[88px]"
|
||||||
>
|
>
|
||||||
{deleteMutation.isPending ? "Deleting…" : "Delete"}
|
<span className={deleteMutation.isPending ? "opacity-0" : ""}>Delete</span>
|
||||||
|
{deleteMutation.isPending && <Spinner size="sm" className="absolute" />}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import "./globals.css";
|
||||||
import { RootProvider } from "fumadocs-ui/provider/next";
|
import { RootProvider } from "fumadocs-ui/provider/next";
|
||||||
import { Roboto } from "next/font/google";
|
import { Roboto } from "next/font/google";
|
||||||
import { AnnouncementToastProvider } from "@/components/announcements/AnnouncementToastProvider";
|
import { AnnouncementToastProvider } from "@/components/announcements/AnnouncementToastProvider";
|
||||||
|
import { DesktopUpdateToast } from "@/components/desktop/desktop-update-toast";
|
||||||
import { GlobalLoadingProvider } from "@/components/providers/GlobalLoadingProvider";
|
import { GlobalLoadingProvider } from "@/components/providers/GlobalLoadingProvider";
|
||||||
import { I18nProvider } from "@/components/providers/I18nProvider";
|
import { I18nProvider } from "@/components/providers/I18nProvider";
|
||||||
import { PostHogProvider } from "@/components/providers/PostHogProvider";
|
import { PostHogProvider } from "@/components/providers/PostHogProvider";
|
||||||
|
|
@ -154,6 +155,7 @@ export default function RootLayout({
|
||||||
<GlobalLoadingProvider>{children}</GlobalLoadingProvider>
|
<GlobalLoadingProvider>{children}</GlobalLoadingProvider>
|
||||||
</ZeroProvider>
|
</ZeroProvider>
|
||||||
</ReactQueryClientProvider>
|
</ReactQueryClientProvider>
|
||||||
|
<DesktopUpdateToast />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<AnnouncementToastProvider />
|
<AnnouncementToastProvider />
|
||||||
</RootProvider>
|
</RootProvider>
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,17 @@ import { EnumConnectorName } from "@/contracts/enums/connector";
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||||
|
import { getReauthEndpoint } from "@/lib/connector-telemetry";
|
||||||
|
import { BACKEND_URL } from "@/lib/env-config";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { DateRangeSelector } from "../../components/date-range-selector";
|
import { DateRangeSelector } from "../../components/date-range-selector";
|
||||||
import { PeriodicSyncConfig } from "../../components/periodic-sync-config";
|
import { PeriodicSyncConfig } from "../../components/periodic-sync-config";
|
||||||
import { SummaryConfig } from "../../components/summary-config";
|
import { SummaryConfig } from "../../components/summary-config";
|
||||||
import { VisionLLMConfig } from "../../components/vision-llm-config";
|
import { VisionLLMConfig } from "../../components/vision-llm-config";
|
||||||
import { getReauthEndpoint, LIVE_CONNECTOR_TYPES } from "../../constants/connector-constants";
|
import { LIVE_CONNECTOR_TYPES } from "../../constants/connector-constants";
|
||||||
import { getConnectorDisplayName } from "../../tabs/all-connectors-tab";
|
import { getConnectorDisplayName } from "../../tabs/all-connectors-tab";
|
||||||
import { MCPServiceConfig } from "../components/mcp-service-config";
|
import { MCPServiceConfig } from "../components/mcp-service-config";
|
||||||
import { getConnectorConfigComponent } from "../index";
|
import { getConnectorConfigComponent } from "../index";
|
||||||
import { BACKEND_URL } from "@/lib/env-config";
|
|
||||||
const VISION_LLM_CONNECTOR_TYPES = new Set<SearchSourceConnector["connector_type"]>([
|
const VISION_LLM_CONNECTOR_TYPES = new Set<SearchSourceConnector["connector_type"]>([
|
||||||
EnumConnectorName.GOOGLE_DRIVE_CONNECTOR,
|
EnumConnectorName.GOOGLE_DRIVE_CONNECTOR,
|
||||||
EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
import { EnumConnectorName } from "@/contracts/enums/connector";
|
||||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connectors that operate in real time (no background indexing).
|
* Connectors that operate in real time (no background indexing).
|
||||||
|
|
@ -294,25 +293,5 @@ export const AUTO_INDEX_DEFAULTS: Record<string, AutoIndexConfig> = {
|
||||||
|
|
||||||
export const AUTO_INDEX_CONNECTOR_TYPES = new Set<string>(Object.keys(AUTO_INDEX_DEFAULTS));
|
export const AUTO_INDEX_CONNECTOR_TYPES = new Set<string>(Object.keys(AUTO_INDEX_DEFAULTS));
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// CONNECTOR TELEMETRY REGISTRY
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// Single source of truth for "what does this connector_type look like in
|
|
||||||
// analytics?". Any connector added to the lists above is automatically
|
|
||||||
// picked up here, so adding a new integration does NOT require touching
|
|
||||||
// `lib/posthog/events.ts` or per-connector tracking code.
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// Telemetry types & helpers are now defined in `@/lib/connector-telemetry`.
|
|
||||||
// Re-exported here for backward compatibility with existing imports.
|
|
||||||
export type {
|
|
||||||
ConnectorTelemetryGroup,
|
|
||||||
ConnectorTelemetryMeta,
|
|
||||||
} from "@/lib/connector-telemetry";
|
|
||||||
export {
|
|
||||||
getConnectorTelemetryMeta,
|
|
||||||
getReauthEndpoint,
|
|
||||||
} from "@/lib/connector-telemetry";
|
|
||||||
|
|
||||||
// Re-export IndexingConfigState from schemas for backward compatibility
|
// Re-export IndexingConfigState from schemas for backward compatibility
|
||||||
export type { IndexingConfigState } from "./connector-popup.schemas";
|
export type { IndexingConfigState } from "./connector-popup.schemas";
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,13 @@ import { EnumConnectorName } from "@/contracts/enums/connector";
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||||
|
import { getReauthEndpoint } from "@/lib/connector-telemetry";
|
||||||
|
import { BACKEND_URL } from "@/lib/env-config";
|
||||||
import { formatRelativeDate } from "@/lib/format-date";
|
import { formatRelativeDate } from "@/lib/format-date";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { getReauthEndpoint, LIVE_CONNECTOR_TYPES } from "../constants/connector-constants";
|
import { LIVE_CONNECTOR_TYPES } from "../constants/connector-constants";
|
||||||
import { useConnectorStatus } from "../hooks/use-connector-status";
|
import { useConnectorStatus } from "../hooks/use-connector-status";
|
||||||
import { getConnectorDisplayName } from "../tabs/all-connectors-tab";
|
import { getConnectorDisplayName } from "../tabs/all-connectors-tab";
|
||||||
import { BACKEND_URL } from "@/lib/env-config";
|
|
||||||
interface ConnectorAccountsListViewProps {
|
interface ConnectorAccountsListViewProps {
|
||||||
connectorType: string;
|
connectorType: string;
|
||||||
connectorTitle: string;
|
connectorTitle: string;
|
||||||
|
|
|
||||||
82
surfsense_web/components/desktop/desktop-update-toast.tsx
Normal file
82
surfsense_web/components/desktop/desktop-update-toast.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Download, X } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type UpdateToastState = {
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DesktopUpdateToast() {
|
||||||
|
const [update, setUpdate] = useState<UpdateToastState | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const api = window.electronAPI;
|
||||||
|
if (!api?.onUpdateDownloaded) return;
|
||||||
|
|
||||||
|
return api.onUpdateDownloaded(({ version }) => {
|
||||||
|
setUpdate({ version });
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!update) return null;
|
||||||
|
|
||||||
|
const installAndRestart = () => {
|
||||||
|
void window.electronAPI?.installUpdateNow();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none fixed bottom-5 right-5 z-[100]">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-auto relative flex w-[360px] max-w-[calc(100vw-2.5rem)] gap-3 rounded-md border border-popover-border",
|
||||||
|
"bg-popover p-4 text-popover-foreground shadow-md"
|
||||||
|
)}
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<div className="mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full text-muted-foreground">
|
||||||
|
<Download className="size-5" strokeWidth={1.8} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="pr-8 text-sm font-semibold tracking-tight">Update available</div>
|
||||||
|
<p className="mt-1.5 text-sm leading-relaxed text-muted-foreground">
|
||||||
|
A new version of SurfSense ({update.version}) is now available to install.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-3 flex items-center gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-auto px-0 text-sm font-semibold hover:bg-transparent hover:text-foreground"
|
||||||
|
onClick={installAndRestart}
|
||||||
|
>
|
||||||
|
Install and restart
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-auto px-0 text-sm font-semibold text-muted-foreground hover:bg-transparent hover:text-foreground"
|
||||||
|
onClick={() => setUpdate(null)}
|
||||||
|
>
|
||||||
|
Not now
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute right-2 top-2 size-7 text-muted-foreground hover:bg-transparent hover:text-foreground"
|
||||||
|
aria-label="Dismiss update toast"
|
||||||
|
onClick={() => setUpdate(null)}
|
||||||
|
>
|
||||||
|
<X className="size-4" strokeWidth={1.8} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
import { EnumConnectorName } from "@/contracts/enums/connector";
|
||||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||||
import {
|
import {
|
||||||
OAUTH_CONNECTORS,
|
|
||||||
COMPOSIO_CONNECTORS,
|
COMPOSIO_CONNECTORS,
|
||||||
CRAWLERS,
|
CRAWLERS,
|
||||||
|
OAUTH_CONNECTORS,
|
||||||
OTHER_CONNECTORS,
|
OTHER_CONNECTORS,
|
||||||
} from "@/components/assistant-ui/connector-popup/constants/connector-constants";
|
} from "@/components/assistant-ui/connector-popup/constants/connector-constants";
|
||||||
|
|
||||||
|
|
|
||||||
6
surfsense_web/types/window.d.ts
vendored
6
surfsense_web/types/window.d.ts
vendored
|
|
@ -83,6 +83,10 @@ interface LocalTextFileResult {
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UpdateDownloadedEvent {
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface ElectronAPI {
|
interface ElectronAPI {
|
||||||
versions: {
|
versions: {
|
||||||
electron: string;
|
electron: string;
|
||||||
|
|
@ -92,6 +96,8 @@ interface ElectronAPI {
|
||||||
};
|
};
|
||||||
openExternal: (url: string) => void;
|
openExternal: (url: string) => void;
|
||||||
getAppVersion: () => Promise<string>;
|
getAppVersion: () => Promise<string>;
|
||||||
|
onUpdateDownloaded: (callback: (data: UpdateDownloadedEvent) => void) => () => void;
|
||||||
|
installUpdateNow: () => Promise<void>;
|
||||||
onDeepLink: (callback: (url: string) => void) => () => void;
|
onDeepLink: (callback: (url: string) => void) => () => void;
|
||||||
onChatScreenCapture: (callback: (dataUrl: string) => void) => () => void;
|
onChatScreenCapture: (callback: (dataUrl: string) => void) => () => void;
|
||||||
getQuickAskText: () => Promise<string>;
|
getQuickAskText: () => Promise<string>;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue