Merge branch 'MODSetter:dev' into dev

This commit is contained in:
Eric Lammertsma 2026-03-04 14:38:01 -05:00 committed by GitHub
commit 4bfcd91f3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 977 additions and 47 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ node_modules/
.ruff_cache/
.venv
.pnpm-store
.DS_Store

View file

@ -81,15 +81,20 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
Ejecuta SurfSense en tu propia infraestructura para control total de datos y privacidad.
**Requisitos previos:** [Docker](https://docs.docker.com/get-docker/) (con [Docker Compose](https://docs.docker.com/compose/install/)) debe estar instalado y en ejecución.
**Requisitos previos:** [Docker Desktop](https://www.docker.com/products/docker-desktop/) debe estar instalado y en ejecución.
> [!NOTE]
> Usuarios de Windows: instalen [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) primero y ejecuten el siguiente comando en la terminal de Ubuntu.
#### Para usuarios de Linux/MacOS:
```bash
curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash
```
#### Para usuarios de Windows:
```powershell
irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex
```
El script de instalación configura [Watchtower](https://github.com/nicholas-fedor/watchtower) automáticamente para actualizaciones diarias. Para omitirlo, agrega la bandera `--no-watchtower`.
Para Docker Compose, instalación manual y otras opciones de despliegue, consulta la [documentación](https://www.surfsense.com/docs/).

View file

@ -81,15 +81,20 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
पूर्ण डेटा नियंत्रण और गोपनीयता के लिए SurfSense को अपने स्वयं के बुनियादी ढांचे पर चलाएं।
**आवश्यकताएँ:** [Docker](https://docs.docker.com/get-docker/) ([Docker Compose](https://docs.docker.com/compose/install/) सहित) इंस्टॉल और चालू होना चाहिए।
**आवश्यकताएँ:** [Docker Desktop](https://www.docker.com/products/docker-desktop/) इंस्टॉल और चालू होना चाहिए।
> [!NOTE]
> Windows उपयोगकर्ता: पहले [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) इंस्टॉल करें और नीचे दिया गया कमांड Ubuntu टर्मिनल में चलाएं।
#### Linux/MacOS उपयोगकर्ताओं के लिए:
```bash
curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash
```
#### Windows उपयोगकर्ताओं के लिए:
```powershell
irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex
```
इंस्टॉल स्क्रिप्ट दैनिक ऑटो-अपडेट के लिए स्वचालित रूप से [Watchtower](https://github.com/nicholas-fedor/watchtower) सेटअप करती है। इसे छोड़ने के लिए, `--no-watchtower` फ्लैग जोड़ें।
Docker Compose, मैनुअल इंस्टॉलेशन और अन्य डिप्लॉयमेंट विकल्पों के लिए, [डॉक्स](https://www.surfsense.com/docs/) देखें।

View file

@ -81,15 +81,20 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
Run SurfSense on your own infrastructure for full data control and privacy.
**Prerequisites:** [Docker](https://docs.docker.com/get-docker/) (with [Docker Compose](https://docs.docker.com/compose/install/)) must be installed and running.
**Prerequisites:** [Docker Desktop](https://www.docker.com/products/docker-desktop/) must be installed and running.
> [!NOTE]
> Windows users: install [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) first and run the command below in the Ubuntu terminal.
#### For Linux/MacOS users:
```bash
curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash
```
#### For Windows users:
```bash
irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex
```
The install script sets up [Watchtower](https://github.com/nicholas-fedor/watchtower) automatically for daily auto-updates. To skip it, add the `--no-watchtower` flag.
For Docker Compose, manual installation, and other deployment options, see the [docs](https://www.surfsense.com/docs/).

View file

@ -81,15 +81,20 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
Execute o SurfSense na sua própria infraestrutura para controle total de dados e privacidade.
**Pré-requisitos:** [Docker](https://docs.docker.com/get-docker/) (com [Docker Compose](https://docs.docker.com/compose/install/)) deve estar instalado e em execução.
**Pré-requisitos:** [Docker Desktop](https://www.docker.com/products/docker-desktop/) deve estar instalado e em execução.
> [!NOTE]
> Usuários do Windows: instalem o [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) primeiro e executem o comando abaixo no terminal do Ubuntu.
#### Para usuários de Linux/MacOS:
```bash
curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash
```
#### Para usuários do Windows:
```powershell
irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex
```
O script de instalação configura o [Watchtower](https://github.com/nicholas-fedor/watchtower) automaticamente para atualizações diárias. Para pular, adicione a flag `--no-watchtower`.
Para Docker Compose, instalação manual e outras opções de implantação, consulte a [documentação](https://www.surfsense.com/docs/).

View file

@ -81,15 +81,20 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
在您自己的基础设施上运行 SurfSense实现完全的数据控制和隐私保护。
**前置条件:** 需要安装并运行 [Docker](https://docs.docker.com/get-docker/)(含 [Docker Compose](https://docs.docker.com/compose/install/)
**前置条件:** 需要安装并运行 [Docker Desktop](https://www.docker.com/products/docker-desktop/)
> [!NOTE]
> Windows 用户:请先安装 [WSL](https://learn.microsoft.com/en-us/windows/wsl/install),然后在 Ubuntu 终端中运行以下命令。
#### Linux/MacOS 用户:
```bash
curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash
```
#### Windows 用户:
```powershell
irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex
```
安装脚本会自动配置 [Watchtower](https://github.com/nicholas-fedor/watchtower) 以实现每日自动更新。如需跳过,请添加 `--no-watchtower` 参数。
如需 Docker Compose、手动安装及其他部署方式请查看[文档](https://www.surfsense.com/docs/)。

350
docker/scripts/install.ps1 Normal file
View file

@ -0,0 +1,350 @@
# =============================================================================
# SurfSense — One-line Install Script (Windows / PowerShell)
#
#
# Usage: irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex
#
# To pass flags, save and run locally:
# .\install.ps1 -NoWatchtower
# .\install.ps1 -WatchtowerInterval 3600
#
# Handles two cases automatically:
# 1. Fresh install — no prior SurfSense data detected
# 2. Migration from the legacy all-in-one container (surfsense-data volume)
# Downloads and runs migrate-database.sh --yes, then restores the dump
# into the new PostgreSQL 17 stack. The user runs one command for both.
# =============================================================================
param(
[switch]$NoWatchtower,
[int]$WatchtowerInterval = 86400
)
$ErrorActionPreference = 'Stop'
# ── Configuration ───────────────────────────────────────────────────────────
$RepoRaw = "https://raw.githubusercontent.com/MODSetter/SurfSense/main"
$InstallDir = ".\surfsense"
$OldVolume = "surfsense-data"
$DumpFile = ".\surfsense_migration_backup.sql"
$KeyFile = ".\surfsense_migration_secret.key"
$MigrationDoneFile = "$InstallDir\.migration_done"
$MigrationMode = $false
$SetupWatchtower = -not $NoWatchtower
$WatchtowerContainer = "watchtower"
# ── Output helpers ──────────────────────────────────────────────────────────
function Write-Info { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Cyan -NoNewline; Write-Host $Msg }
function Write-Ok { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Green -NoNewline; Write-Host $Msg }
function Write-Warn { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Yellow -NoNewline; Write-Host $Msg }
function Write-Step { param([string]$Msg) Write-Host "`n-- $Msg" -ForegroundColor Cyan }
function Write-Err { param([string]$Msg) Write-Host "[SurfSense] ERROR: $Msg" -ForegroundColor Red; exit 1 }
function Invoke-NativeSafe {
param([scriptblock]$Command)
$previousErrorActionPreference = $ErrorActionPreference
try {
$ErrorActionPreference = 'Continue'
& $Command
} finally {
$ErrorActionPreference = $previousErrorActionPreference
}
}
# ── Pre-flight checks ──────────────────────────────────────────────────────
Write-Step "Checking prerequisites"
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
Write-Err "Docker is not installed. Install Docker Desktop: https://docs.docker.com/desktop/install/windows-install/"
}
Write-Ok "Docker found."
Invoke-NativeSafe { docker info *>$null } | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Err "Docker daemon is not running. Please start Docker Desktop and try again."
}
Write-Ok "Docker daemon is running."
Invoke-NativeSafe { docker compose version *>$null } | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Err "Docker Compose is not available. It should be bundled with Docker Desktop."
}
Write-Ok "Docker Compose found."
# ── Wait-for-postgres helper ────────────────────────────────────────────────
function Wait-ForPostgres {
param([string]$DbUser)
$maxAttempts = 45
$attempt = 0
Write-Info "Waiting for PostgreSQL to accept connections..."
do {
$attempt++
if ($attempt -ge $maxAttempts) {
Write-Err "PostgreSQL did not become ready after $($maxAttempts * 2) seconds.`nCheck logs: cd $InstallDir; docker compose logs db"
}
Start-Sleep -Seconds 2
Push-Location $InstallDir
Invoke-NativeSafe { docker compose exec -T db pg_isready -U $DbUser -q *>$null } | Out-Null
$ready = $LASTEXITCODE -eq 0
Pop-Location
} while (-not $ready)
Write-Ok "PostgreSQL is ready."
}
# ── Download files ──────────────────────────────────────────────────────────
Write-Step "Downloading SurfSense files"
Write-Info "Installation directory: $InstallDir"
New-Item -ItemType Directory -Path "$InstallDir\scripts" -Force | Out-Null
$Files = @(
@{ Src = "docker/docker-compose.yml"; Dest = "docker-compose.yml" }
@{ Src = "docker/.env.example"; Dest = ".env.example" }
@{ Src = "docker/postgresql.conf"; Dest = "postgresql.conf" }
@{ Src = "docker/scripts/init-electric-user.sh"; Dest = "scripts/init-electric-user.sh" }
@{ Src = "docker/scripts/migrate-database.ps1"; Dest = "scripts/migrate-database.ps1" }
)
foreach ($f in $Files) {
$destPath = Join-Path $InstallDir $f.Dest
Write-Info "Downloading $($f.Dest)..."
try {
Invoke-WebRequest -Uri "$RepoRaw/$($f.Src)" -OutFile $destPath -UseBasicParsing
} catch {
Write-Err "Failed to download $($f.Dest). Check your internet connection and try again."
}
}
Write-Ok "All files downloaded to $InstallDir/"
# ── Legacy all-in-one detection ─────────────────────────────────────────────
$volumeList = Invoke-NativeSafe { docker volume ls --format '{{.Name}}' 2>$null }
if (($volumeList -split "`n") -contains $OldVolume -and -not (Test-Path $MigrationDoneFile)) {
$MigrationMode = $true
if (Test-Path $DumpFile) {
Write-Step "Migration mode - using existing dump (skipping extraction)"
Write-Info "Found existing dump: $DumpFile"
Write-Info "Skipping data extraction - proceeding directly to restore."
Write-Info "To force a fresh extraction, remove the dump first: Remove-Item $DumpFile"
} else {
Write-Step "Migration mode - legacy all-in-one container detected"
Write-Warn "Volume '$OldVolume' found. Your data will be migrated automatically."
Write-Warn "PostgreSQL is being upgraded from version 14 to 17."
Write-Warn "Your original data will NOT be deleted."
Write-Host ""
Write-Info "Running data extraction (migrate-database.ps1 -Yes)..."
Write-Info "Full extraction log: ./surfsense-migration.log"
Write-Host ""
$migrateScript = Join-Path $InstallDir "scripts/migrate-database.ps1"
& $migrateScript -Yes
if ($LASTEXITCODE -ne 0) {
Write-Err "Data extraction failed. See ./surfsense-migration.log for details.`nYou can also run migrate-database.ps1 manually with custom flags."
}
Write-Host ""
Write-Ok "Data extraction complete. Proceeding with installation and restore."
}
}
# ── Set up .env ─────────────────────────────────────────────────────────────
Write-Step "Configuring environment"
$envPath = Join-Path $InstallDir ".env"
$envExamplePath = Join-Path $InstallDir ".env.example"
if (-not (Test-Path $envPath)) {
Copy-Item $envExamplePath $envPath
if ($MigrationMode -and (Test-Path $KeyFile)) {
$SecretKey = (Get-Content $KeyFile -Raw).Trim()
Write-Ok "Using SECRET_KEY recovered from legacy container."
} else {
$bytes = New-Object byte[] 32
$rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new()
$rng.GetBytes($bytes)
$rng.Dispose()
$SecretKey = [Convert]::ToBase64String($bytes)
Write-Ok "Generated new random SECRET_KEY."
}
$content = Get-Content $envPath -Raw
$content = $content -replace 'SECRET_KEY=replace_me_with_a_random_string', "SECRET_KEY=$SecretKey"
Set-Content -Path $envPath -Value $content -NoNewline
Write-Info "Created $envPath"
} else {
Write-Warn ".env already exists - keeping your existing configuration."
}
# ── Start containers ────────────────────────────────────────────────────────
if ($MigrationMode) {
$envContent = Get-Content $envPath
$DbUser = ($envContent | Select-String '^DB_USER=' | ForEach-Object { ($_ -split '=',2)[1].Trim('"') }) | Select-Object -First 1
$DbPass = ($envContent | Select-String '^DB_PASSWORD=' | ForEach-Object { ($_ -split '=',2)[1].Trim('"') }) | Select-Object -First 1
$DbName = ($envContent | Select-String '^DB_NAME=' | ForEach-Object { ($_ -split '=',2)[1].Trim('"') }) | Select-Object -First 1
if (-not $DbUser) { $DbUser = "surfsense" }
if (-not $DbPass) { $DbPass = "surfsense" }
if (-not $DbName) { $DbName = "surfsense" }
Write-Step "Starting PostgreSQL 17"
Push-Location $InstallDir
Invoke-NativeSafe { docker compose up -d db } | Out-Null
Pop-Location
Wait-ForPostgres -DbUser $DbUser
Write-Step "Restoring database"
if (-not (Test-Path $DumpFile)) {
Write-Err "Dump file '$DumpFile' not found. The migration script may have failed."
}
$DumpFilePath = (Resolve-Path $DumpFile).Path
Write-Info "Restoring dump into PostgreSQL 17 - this may take a while for large databases..."
$restoreErrFile = Join-Path $env:TEMP "surfsense_restore_err.log"
Push-Location $InstallDir
Invoke-NativeSafe { Get-Content -LiteralPath $DumpFilePath | docker compose exec -T -e "PGPASSWORD=$DbPass" db psql -U $DbUser -d $DbName 2>$restoreErrFile | Out-Null } | Out-Null
Pop-Location
$fatalErrors = @()
if (Test-Path $restoreErrFile) {
$fatalErrors = Get-Content $restoreErrFile |
Where-Object { $_ -match '^ERROR:' } |
Where-Object { $_ -notmatch 'already exists' } |
Where-Object { $_ -notmatch 'multiple primary keys' }
}
if ($fatalErrors.Count -gt 0) {
Write-Warn "Restore completed with errors (may be harmless pg_dump header noise):"
$fatalErrors | ForEach-Object { Write-Host $_ }
Write-Warn "If SurfSense behaves incorrectly, inspect manually."
} else {
Write-Ok "Database restored with no fatal errors."
}
# Smoke test
Push-Location $InstallDir
$tableCount = (Invoke-NativeSafe { docker compose exec -T -e "PGPASSWORD=$DbPass" db psql -U $DbUser -d $DbName -t -c "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>$null }).Trim()
Pop-Location
if (-not $tableCount -or $tableCount -eq "0") {
Write-Warn "Smoke test: no tables found after restore."
Write-Warn "The restore may have failed silently. Check: cd $InstallDir; docker compose logs db"
} else {
Write-Ok "Smoke test passed: $tableCount table(s) restored successfully."
New-Item -Path $MigrationDoneFile -ItemType File -Force | Out-Null
}
Write-Step "Starting all SurfSense services"
Push-Location $InstallDir
Invoke-NativeSafe { docker compose up -d }
Pop-Location
Write-Ok "All services started."
Remove-Item $KeyFile -ErrorAction SilentlyContinue
} else {
Write-Step "Starting SurfSense"
Push-Location $InstallDir
Invoke-NativeSafe { docker compose up -d }
Pop-Location
Write-Ok "All services started."
}
# ── Watchtower (auto-update) ────────────────────────────────────────────────
if ($SetupWatchtower) {
$wtHours = [math]::Floor($WatchtowerInterval / 3600)
Write-Step "Setting up Watchtower (auto-updates every ${wtHours}h)"
$wtState = Invoke-NativeSafe { docker inspect -f '{{.State.Running}}' $WatchtowerContainer 2>$null }
if ($LASTEXITCODE -ne 0) { $wtState = "missing" }
if ($wtState -eq "true") {
Write-Ok "Watchtower is already running - skipping."
} else {
if ($wtState -ne "missing") {
Write-Info "Removing stopped Watchtower container..."
Invoke-NativeSafe { docker rm -f $WatchtowerContainer *>$null } | Out-Null
}
Invoke-NativeSafe {
docker run -d `
--name $WatchtowerContainer `
--restart unless-stopped `
-v /var/run/docker.sock:/var/run/docker.sock `
nickfedor/watchtower `
--label-enable `
--interval $WatchtowerInterval *>$null
} | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Ok "Watchtower started - labeled SurfSense containers will auto-update."
} else {
Write-Warn "Could not start Watchtower. You can set it up manually or use: docker compose pull; docker compose up -d"
}
}
} else {
Write-Info "Skipping Watchtower setup (-NoWatchtower flag)."
}
# ── Done ────────────────────────────────────────────────────────────────────
Write-Host ""
Write-Host @"
.d8888b. .d888 .d8888b.
d88P Y88b d88P" d88P Y88b
Y88b. 888 Y88b.
"Y888b. 888 888 888d888 888888 "Y888b. .d88b. 88888b. .d8888b .d88b.
"Y88b. 888 888 888P" 888 "Y88b. d8P Y8b 888 "88b 88K d8P Y8b
"888 888 888 888 888 "888 88888888 888 888 "Y8888b. 88888888
Y88b d88P Y88b 888 888 888 Y88b d88P Y8b. 888 888 X88 Y8b.
"Y8888P" "Y88888 888 888 "Y8888P" "Y8888 888 888 88888P' "Y8888
"@ -ForegroundColor White
$versionDisplay = (Get-Content $envPath | Select-String '^SURFSENSE_VERSION=' | ForEach-Object { ($_ -split '=',2)[1].Trim('"') }) | Select-Object -First 1
if (-not $versionDisplay) { $versionDisplay = "latest" }
Write-Host " OSS Alternative to NotebookLM for Teams [$versionDisplay]" -ForegroundColor Yellow
Write-Host ("=" * 62) -ForegroundColor Cyan
Write-Host ""
Write-Info " Frontend: http://localhost:3000"
Write-Info " Backend: http://localhost:8000"
Write-Info " API Docs: http://localhost:8000/docs"
Write-Info ""
Write-Info " Config: $InstallDir\.env"
Write-Info " Logs: cd $InstallDir; docker compose logs -f"
Write-Info " Stop: cd $InstallDir; docker compose down"
Write-Info " Update: cd $InstallDir; docker compose pull; docker compose up -d"
Write-Info ""
if ($SetupWatchtower) {
Write-Info " Watchtower: auto-updates every ${wtHours}h (stop: docker rm -f $WatchtowerContainer)"
} else {
Write-Warn " Watchtower skipped. For auto-updates, re-run without -NoWatchtower."
}
Write-Info ""
if ($MigrationMode) {
Write-Warn " Migration complete! Open frontend and verify your data."
Write-Warn " Once verified, clean up the legacy volume and migration files:"
Write-Warn " docker volume rm $OldVolume"
Write-Warn " Remove-Item $DumpFile"
Write-Warn " Remove-Item $MigrationDoneFile"
} else {
Write-Warn " First startup may take a few minutes while images are pulled."
Write-Warn " Edit $InstallDir\.env to configure API keys, OAuth, etc."
}

View file

@ -25,7 +25,7 @@ set -euo pipefail
main() {
REPO_RAW="https://raw.githubusercontent.com/MODSetter/SurfSense/dev"
REPO_RAW="https://raw.githubusercontent.com/MODSetter/SurfSense/main"
INSTALL_DIR="./surfsense"
OLD_VOLUME="surfsense-data"
DUMP_FILE="./surfsense_migration_backup.sql"
@ -299,12 +299,9 @@ Y88b d88P Y88b 888 888 888 Y88b d88P Y8b. 888 888 X88 Y8b.
EOF
if [[ "${SURFSENSE_VERSION:-latest}" == "latest" ]]; then
_version_display="latest"
else
_version_display="v${SURFSENSE_VERSION}"
fi
printf " Your personal AI-powered search engine ${YELLOW}[%s]${NC}\n" "${_version_display}"
_version_display=$(grep '^SURFSENSE_VERSION=' "${INSTALL_DIR}/.env" 2>/dev/null | cut -d= -f2 | tr -d '"' | head -1 || true)
_version_display="${_version_display:-latest}"
printf " OSS Alternative to NotebookLM for Teams ${YELLOW}[%s]${NC}\n" "${_version_display}"
printf "${CYAN}══════════════════════════════════════════════════════════════${NC}\n\n"
info " Frontend: http://localhost:3000"

View file

@ -0,0 +1,343 @@
# =============================================================================
# SurfSense — Database Migration Script (Windows / PowerShell)
#
# Extracts data from the legacy all-in-one surfsense-data volume (PostgreSQL 14)
# and saves it as a SQL dump + SECRET_KEY file ready for install.ps1 to restore.
#
# Usage:
# .\migrate-database.ps1 [options]
#
# Options:
# -DbUser USER Old PostgreSQL username (default: surfsense)
# -DbPassword PASS Old PostgreSQL password (default: surfsense)
# -DbName NAME Old PostgreSQL database (default: surfsense)
# -Yes Skip all confirmation prompts
#
# Prerequisites:
# - Docker Desktop installed and running
# - The legacy surfsense-data volume must exist
# - ~500 MB free disk space for the dump file
#
# What this script does:
# 1. Stops any container using surfsense-data (to prevent corruption)
# 2. Starts a temporary PG14 container against the old volume
# 3. Dumps the database to .\surfsense_migration_backup.sql
# 4. Recovers the SECRET_KEY to .\surfsense_migration_secret.key
# 5. Exits — leaving installation to install.ps1
#
# What this script does NOT do:
# - Delete the original surfsense-data volume (do this manually after verifying)
# - Install the new SurfSense stack (install.ps1 handles that automatically)
#
# Note:
# install.ps1 downloads and runs this script automatically when it detects the
# legacy surfsense-data volume. You only need to run this script manually if
# you have custom database credentials (-DbUser / -DbPassword / -DbName)
# or if the automatic migration inside install.ps1 fails at the extraction step.
# =============================================================================
param(
[string]$DbUser = "surfsense",
[string]$DbPassword = "surfsense",
[string]$DbName = "surfsense",
[switch]$Yes
)
$ErrorActionPreference = 'Stop'
# ── Constants ────────────────────────────────────────────────────────────────
$OldVolume = "surfsense-data"
$TempContainer = "surfsense-pg14-migration"
$DumpFile = ".\surfsense_migration_backup.sql"
$KeyFile = ".\surfsense_migration_secret.key"
$PG14Image = "pgvector/pgvector:pg14"
$LogFile = ".\surfsense-migration.log"
# ── Output helpers ───────────────────────────────────────────────────────────
function Write-Info { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Cyan -NoNewline; Write-Host $Msg }
function Write-Ok { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Green -NoNewline; Write-Host $Msg }
function Write-Warn { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Yellow -NoNewline; Write-Host $Msg }
function Write-Step { param([string]$Step, [string]$Msg) Write-Host "`n-- Step ${Step}: $Msg" -ForegroundColor Cyan }
function Write-Err { param([string]$Msg) Write-Host "[SurfSense] ERROR: $Msg" -ForegroundColor Red; exit 1 }
function Log { param([string]$Msg) Add-Content -Path $LogFile -Value $Msg }
function Invoke-NativeSafe {
param([scriptblock]$Command)
$previousErrorActionPreference = $ErrorActionPreference
try {
$ErrorActionPreference = 'Continue'
& $Command
} finally {
$ErrorActionPreference = $previousErrorActionPreference
}
}
function Confirm-Action {
param([string]$Prompt)
if ($Yes) { return }
$reply = Read-Host "[SurfSense] $Prompt [y/N]"
if ($reply -notmatch '^[Yy]$') {
Write-Warn "Aborted."
exit 0
}
}
# ── Cleanup helper ───────────────────────────────────────────────────────────
function Remove-TempContainer {
$containers = Invoke-NativeSafe { docker ps -a --format '{{.Names}}' 2>$null }
if ($containers -and ($containers -split "`n") -contains $TempContainer) {
Write-Info "Cleaning up temporary container '$TempContainer'..."
Invoke-NativeSafe { docker stop $TempContainer *>$null } | Out-Null
Invoke-NativeSafe { docker rm $TempContainer *>$null } | Out-Null
}
}
# Register cleanup on script exit
Register-EngineEvent PowerShell.Exiting -Action {
$containers = Invoke-NativeSafe { docker ps -a --format '{{.Names}}' 2>$null }
if ($containers -and ($containers -split "`n") -contains "surfsense-pg14-migration") {
Invoke-NativeSafe { docker stop "surfsense-pg14-migration" *>$null } | Out-Null
Invoke-NativeSafe { docker rm "surfsense-pg14-migration" *>$null } | Out-Null
}
} | Out-Null
# ── Wait-for-postgres helper ────────────────────────────────────────────────
function Wait-ForPostgres {
param(
[string]$Container,
[string]$User,
[string]$Label = "PostgreSQL"
)
$maxAttempts = 45
$attempt = 0
Write-Info "Waiting for $Label to accept connections..."
do {
$attempt++
if ($attempt -ge $maxAttempts) {
Write-Err "$Label did not become ready after $($maxAttempts * 2) seconds. Check: docker logs $Container"
}
Start-Sleep -Seconds 2
Invoke-NativeSafe { docker exec $Container pg_isready -U $User -q 2>$null } | Out-Null
} while ($LASTEXITCODE -ne 0)
Write-Ok "$Label is ready."
}
Write-Info "Migrating data from legacy database (PostgreSQL 14 -> 17)"
"Migration started at $(Get-Date)" | Out-File $LogFile
# ── Step 0: Pre-flight checks ───────────────────────────────────────────────
Write-Step "0" "Pre-flight checks"
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
Write-Err "Docker is not installed. Install Docker Desktop: https://docs.docker.com/desktop/install/windows-install/"
}
Invoke-NativeSafe { docker info *>$null } | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Err "Docker daemon is not running. Please start Docker Desktop and try again."
}
$volumeList = Invoke-NativeSafe { docker volume ls --format '{{.Name}}' 2>$null }
if (-not (($volumeList -split "`n") -contains $OldVolume)) {
Write-Err "Legacy volume '$OldVolume' not found. Are you sure you ran the old all-in-one SurfSense container?"
}
Write-Ok "Found legacy volume: $OldVolume"
$oldContainer = (Invoke-NativeSafe { docker ps --filter "volume=$OldVolume" --format '{{.Names}}' 2>$null } | Select-Object -First 1)
if ($oldContainer) {
Write-Warn "Container '$oldContainer' is running and using the '$OldVolume' volume."
Write-Warn "It must be stopped before migration to prevent data file corruption."
Confirm-Action "Stop '$oldContainer' now and proceed with data extraction?"
Invoke-NativeSafe { docker stop $oldContainer *>$null } | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Err "Failed to stop '$oldContainer'. Try: docker stop $oldContainer"
}
Write-Ok "Container '$oldContainer' stopped."
}
if (Test-Path $DumpFile) {
Write-Warn "Dump file '$DumpFile' already exists."
Write-Warn "If a previous extraction succeeded, just run install.ps1 now."
Write-Warn "To re-extract, remove the file first: Remove-Item $DumpFile"
Write-Err "Aborting to avoid overwriting an existing dump."
}
$staleContainers = Invoke-NativeSafe { docker ps -a --format '{{.Names}}' 2>$null }
if ($staleContainers -and ($staleContainers -split "`n") -contains $TempContainer) {
Write-Warn "Stale migration container '$TempContainer' found - removing it."
Invoke-NativeSafe { docker stop $TempContainer *>$null } | Out-Null
Invoke-NativeSafe { docker rm $TempContainer *>$null } | Out-Null
}
$drive = (Get-Item .).PSDrive
$freeMB = [math]::Floor($drive.Free / 1MB)
if ($freeMB -lt 500) {
Write-Warn "Low disk space: $freeMB MB free. At least 500 MB recommended for the dump."
Confirm-Action "Continue anyway?"
} else {
Write-Ok "Disk space: $freeMB MB free."
}
Write-Ok "All pre-flight checks passed."
# ── Confirmation prompt ──────────────────────────────────────────────────────
Write-Host ""
Write-Host "Extraction plan:" -ForegroundColor White
Write-Host " Source volume : " -NoNewline; Write-Host "$OldVolume" -ForegroundColor Yellow -NoNewline; Write-Host " (PG14 data at /data/postgres)"
Write-Host " Old credentials : user=" -NoNewline; Write-Host "$DbUser" -ForegroundColor Yellow -NoNewline; Write-Host " db=" -NoNewline; Write-Host "$DbName" -ForegroundColor Yellow
Write-Host " Dump saved to : " -NoNewline; Write-Host "$DumpFile" -ForegroundColor Yellow
Write-Host " SECRET_KEY to : " -NoNewline; Write-Host "$KeyFile" -ForegroundColor Yellow
Write-Host " Log file : " -NoNewline; Write-Host "$LogFile" -ForegroundColor Yellow
Write-Host ""
Confirm-Action "Start data extraction? (Your original data will not be deleted or modified.)"
# ── Step 1: Start temporary PostgreSQL 14 container ──────────────────────────
Write-Step "1" "Starting temporary PostgreSQL 14 container"
Write-Info "Pulling $PG14Image..."
Invoke-NativeSafe { docker pull $PG14Image *>$null } | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warn "Could not pull $PG14Image - using cached image if available."
}
$dataUid = Invoke-NativeSafe { docker run --rm -v "${OldVolume}:/data" alpine stat -c '%u' /data/postgres 2>$null }
if (-not $dataUid -or $dataUid -eq "0") {
Write-Warn "Could not detect data directory UID - falling back to default (may chown files)."
$userFlag = @()
} else {
Write-Info "Data directory owned by UID $dataUid - starting temp container as that user."
$userFlag = @("--user", $dataUid)
}
$dockerRunArgs = @(
"run", "-d",
"--name", $TempContainer,
"-v", "${OldVolume}:/data",
"-e", "PGDATA=/data/postgres",
"-e", "POSTGRES_USER=$DbUser",
"-e", "POSTGRES_PASSWORD=$DbPassword",
"-e", "POSTGRES_DB=$DbName"
) + $userFlag + @($PG14Image)
Invoke-NativeSafe { docker @dockerRunArgs *>$null } | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Err "Failed to start temporary PostgreSQL 14 container."
}
Write-Ok "Temporary container '$TempContainer' started."
Wait-ForPostgres -Container $TempContainer -User $DbUser -Label "PostgreSQL 14"
# ── Step 2: Dump the database ────────────────────────────────────────────────
Write-Step "2" "Dumping PostgreSQL 14 database"
Write-Info "Running pg_dump - this may take a while for large databases..."
$pgDumpErrFile = Join-Path $env:TEMP "surfsense_pgdump_err.log"
Invoke-NativeSafe { docker exec -e "PGPASSWORD=$DbPassword" $TempContainer pg_dump -U $DbUser --no-password $DbName > $DumpFile 2>$pgDumpErrFile } | Out-Null
if ($LASTEXITCODE -ne 0) {
if (Test-Path $pgDumpErrFile) { Get-Content $pgDumpErrFile | Write-Host -ForegroundColor Red }
Remove-TempContainer
Write-Err "pg_dump failed. See above for details."
}
if (-not (Test-Path $DumpFile) -or (Get-Item $DumpFile).Length -eq 0) {
Remove-TempContainer
Write-Err "Dump file '$DumpFile' is empty. Something went wrong with pg_dump."
}
$dumpContent = (Get-Content $DumpFile -TotalCount 5) -join "`n"
if ($dumpContent -notmatch "PostgreSQL database dump") {
Remove-TempContainer
Write-Err "Dump file does not contain a valid PostgreSQL dump header - the file may be corrupt."
}
$dumpLines = (Get-Content $DumpFile | Measure-Object -Line).Lines
if ($dumpLines -lt 10) {
Remove-TempContainer
Write-Err "Dump has only $dumpLines lines - suspiciously small. Aborting."
}
$dumpSize = "{0:N1} MB" -f ((Get-Item $DumpFile).Length / 1MB)
Write-Ok "Dump complete: $dumpSize ($dumpLines lines) -> $DumpFile"
Write-Info "Stopping temporary PostgreSQL 14 container..."
Invoke-NativeSafe { docker stop $TempContainer *>$null } | Out-Null
Invoke-NativeSafe { docker rm $TempContainer *>$null } | Out-Null
Write-Ok "Temporary container removed."
# ── Step 3: Recover SECRET_KEY ───────────────────────────────────────────────
Write-Step "3" "Recovering SECRET_KEY"
$recoveredKey = ""
$keyCheck = Invoke-NativeSafe { docker run --rm -v "${OldVolume}:/data" alpine sh -c 'test -f /data/.secret_key && cat /data/.secret_key' 2>$null }
if ($LASTEXITCODE -eq 0 -and $keyCheck) {
$recoveredKey = $keyCheck.Trim()
Write-Ok "Recovered SECRET_KEY from '$OldVolume'."
} else {
Write-Warn "No SECRET_KEY file found at /data/.secret_key in '$OldVolume'."
Write-Warn "This means the all-in-one container was launched with SECRET_KEY set as an explicit env var."
if ($Yes) {
$bytes = New-Object byte[] 32
$rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new()
$rng.GetBytes($bytes)
$rng.Dispose()
$recoveredKey = [Convert]::ToBase64String($bytes)
Write-Warn "Non-interactive mode: generated a new SECRET_KEY automatically."
Write-Warn "All active browser sessions will be logged out after migration."
Write-Warn "To restore your original key, update SECRET_KEY in .\surfsense\.env afterwards."
} else {
Write-Warn "Enter the SECRET_KEY from your old container's environment"
$recoveredKey = Read-Host "[SurfSense] (press Enter to generate a new one - existing sessions will be invalidated)"
if (-not $recoveredKey) {
$bytes = New-Object byte[] 32
$rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new()
$rng.GetBytes($bytes)
$rng.Dispose()
$recoveredKey = [Convert]::ToBase64String($bytes)
Write-Warn "Generated a new SECRET_KEY. All active browser sessions will be logged out after migration."
}
}
}
Set-Content -Path $KeyFile -Value $recoveredKey -NoNewline
Write-Ok "SECRET_KEY saved to $KeyFile"
# ── Done ─────────────────────────────────────────────────────────────────────
Write-Host ""
Write-Host ("=" * 62) -ForegroundColor Green
Write-Host " Data extraction complete!" -ForegroundColor Green
Write-Host ("=" * 62) -ForegroundColor Green
Write-Host ""
Write-Ok "Dump file : $DumpFile ($dumpSize)"
Write-Ok "Secret key: $KeyFile"
Write-Host ""
Write-Info "Next step - run install.ps1 from this same directory:"
Write-Host ""
Write-Host " irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex" -ForegroundColor Cyan
Write-Host ""
Write-Info "install.ps1 will detect the dump, restore your data into PostgreSQL 17,"
Write-Info "and start the full SurfSense stack automatically."
Write-Host ""
Write-Warn "Keep both files until you have verified the migration:"
Write-Warn " $DumpFile"
Write-Warn " $KeyFile"
Write-Warn "Full log saved to: $LogFile"
Write-Host ""
Log "Migration extraction completed successfully at $(Get-Date)"

View file

@ -12,7 +12,7 @@ from litellm import aspeech
from app.config import config as app_config
from app.services.kokoro_tts_service import get_kokoro_tts_service
from app.services.llm_service import get_document_summary_llm
from app.services.llm_service import get_agent_llm
from .configuration import Configuration
from .prompts import get_podcast_generation_prompt
@ -31,7 +31,7 @@ async def create_podcast_transcript(
user_prompt = configuration.user_prompt
# Get search space's document summary LLM
llm = await get_document_summary_llm(state.db_session, search_space_id)
llm = await get_agent_llm(state.db_session, search_space_id)
if not llm:
error_message = (
f"No document summary LLM configured for search space {search_space_id}"

View file

@ -2,7 +2,7 @@ import { Route, Routes } from "react-router-dom";
import ApiKeyForm from "./pages/ApiKeyForm";
import HomePage from "./pages/HomePage";
import "../tailwind.css";
import "~tailwind.css";
export const Routing = () => (
<Routes>

View file

@ -4,6 +4,8 @@ import { ReloadIcon } from "@radix-ui/react-icons";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "~/routes/ui/button";
import { ConnectionSettingsButton } from "~/routes/ui/connection-settings-button";
import { buildBackendUrl } from "~utils/backend-url";
const ApiKeyForm = () => {
const navigation = useNavigate();
@ -27,8 +29,7 @@ const ApiKeyForm = () => {
setLoading(true);
try {
// Verify token is valid by making a request to the API
const response = await fetch(`${process.env.PLASMO_PUBLIC_BACKEND_URL}/verify-token`, {
const response = await fetch(await buildBackendUrl("/verify-token"), {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
@ -53,6 +54,10 @@ const ApiKeyForm = () => {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 to-gray-800 flex flex-col items-center justify-center p-6">
<div className="w-full max-w-md mx-auto space-y-8">
<div className="flex justify-end">
<ConnectionSettingsButton />
</div>
<div className="flex flex-col items-center space-y-2">
<div className="bg-gray-800 p-3 rounded-full ring-2 ring-gray-700 shadow-lg">
<img className="w-12 h-12" src={icon} alt="SurfSense" />

View file

@ -16,6 +16,7 @@ import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { cn } from "~/lib/utils";
import { Button } from "~/routes/ui/button";
import { ConnectionSettingsButton } from "~/routes/ui/connection-settings-button";
import {
Command,
CommandEmpty,
@ -27,6 +28,7 @@ import {
import { Popover, PopoverContent, PopoverTrigger } from "~/routes/ui/popover";
import { Label } from "~routes/ui/label";
import { useToast } from "~routes/ui/use-toast";
import { buildBackendUrl } from "~utils/backend-url";
import { getRenderedHtml } from "~utils/commons";
import type { WebHistory } from "~utils/interfaces";
import Loading from "./Loading";
@ -45,15 +47,19 @@ const HomePage = () => {
const checkSearchSpaces = async () => {
const storage = new Storage({ area: "local" });
const token = await storage.get("token");
if (!token) {
setLoading(false);
navigation("/login");
return;
}
try {
const response = await fetch(
`${process.env.PLASMO_PUBLIC_BACKEND_URL}/api/v1/searchspaces`,
{
headers: {
Authorization: `Bearer ${token}`,
},
const response = await fetch(await buildBackendUrl("/api/v1/searchspaces"), {
headers: {
Authorization: `Bearer ${token}`,
}
);
});
if (!response.ok) {
throw new Error("Token verification failed");
@ -66,11 +72,12 @@ const HomePage = () => {
await storage.remove("token");
await storage.remove("showShadowDom");
navigation("/login");
} finally {
setLoading(false);
}
};
checkSearchSpaces();
setLoading(false);
}, []);
useEffect(() => {
@ -304,6 +311,19 @@ const HomePage = () => {
navigation("/login");
}
async function handleConnectionSaved(changed: boolean): Promise<void> {
if (!changed) {
return;
}
const storage = new Storage({ area: "local" });
await storage.remove("token");
await storage.remove("showShadowDom");
await storage.remove("search_space");
await storage.remove("search_space_id");
navigation("/login");
}
if (loading) {
return <Loading />;
} else {
@ -344,15 +364,18 @@ const HomePage = () => {
</div>
<h1 className="text-xl font-semibold text-white">SurfSense</h1>
</div>
<Button
variant="ghost"
size="icon"
onClick={logOut}
className="rounded-full text-gray-400 hover:bg-gray-800 hover:text-white"
>
<ExitIcon className="h-4 w-4" />
<span className="sr-only">Log out</span>
</Button>
<div className="flex items-center gap-1">
<ConnectionSettingsButton onSaved={handleConnectionSaved} />
<Button
variant="ghost"
size="icon"
onClick={logOut}
className="rounded-full text-gray-400 hover:bg-gray-800 hover:text-white"
>
<ExitIcon className="h-4 w-4" />
<span className="sr-only">Log out</span>
</Button>
</div>
</div>
<div className="space-y-3 py-4">

View file

@ -0,0 +1,114 @@
import { GearIcon } from "@radix-ui/react-icons";
import { useEffect, useState } from "react";
import { Button } from "~/routes/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/routes/ui/dialog";
import { Label } from "~/routes/ui/label";
import {
DEFAULT_BACKEND_BASE_URL,
getCustomBackendBaseUrl,
normalizeBackendBaseUrl,
setCustomBackendBaseUrl,
} from "~utils/backend-url";
type ConnectionSettingsButtonProps = {
onSaved?: (changed: boolean) => void | Promise<void>;
};
export function ConnectionSettingsButton({ onSaved }: ConnectionSettingsButtonProps) {
const [open, setOpen] = useState(false);
const [customUrl, setCustomUrl] = useState("");
const [savedUrl, setSavedUrl] = useState("");
useEffect(() => {
if (!open) {
return;
}
const loadSettings = async () => {
const normalized = await getCustomBackendBaseUrl();
setCustomUrl(normalized || DEFAULT_BACKEND_BASE_URL);
setSavedUrl(normalized);
};
loadSettings();
}, [open]);
const handleSave = async () => {
const normalizedUrl = normalizeBackendBaseUrl(customUrl);
const nextUrl = await setCustomBackendBaseUrl(
normalizedUrl === DEFAULT_BACKEND_BASE_URL ? "" : normalizedUrl
);
const changed = nextUrl !== savedUrl;
setSavedUrl(nextUrl);
setCustomUrl(nextUrl || DEFAULT_BACKEND_BASE_URL);
setOpen(false);
if (onSaved) {
await onSaved(changed);
}
};
return (
<>
<Button
variant="ghost"
size="icon"
onClick={() => setOpen(true)}
className="rounded-full text-gray-400 hover:bg-gray-800 hover:text-white"
>
<GearIcon className="h-4 w-4" />
<span className="sr-only">Connection settings</span>
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-md border-gray-700 bg-gray-800 text-white">
<DialogHeader>
<DialogTitle>Connection Settings</DialogTitle>
<DialogDescription className="text-gray-400">
Leave blank to use the default SurfSense backend URL.
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<Label htmlFor="backendBaseUrl" className="text-gray-300">
Custom Backend URL
</Label>
<input
id="backendBaseUrl"
type="url"
value={customUrl}
onChange={(event) => setCustomUrl(event.target.value)}
placeholder={DEFAULT_BACKEND_BASE_URL}
className="w-full rounded-md border border-gray-700 bg-gray-900 px-3 py-2 text-white placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-teal-500"
/>
<p className="text-xs text-gray-500">Default: {DEFAULT_BACKEND_BASE_URL}</p>
</div>
<DialogFooter className="gap-2">
<Button
type="button"
variant="outline"
onClick={() => setCustomUrl(DEFAULT_BACKEND_BASE_URL)}
className="border-gray-700 bg-gray-900 text-gray-200 hover:bg-gray-700"
>
Use Default
</Button>
<Button
type="button"
onClick={handleSave}
className="bg-teal-600 text-white hover:bg-teal-500"
>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View file

@ -0,0 +1,41 @@
import { Storage } from "@plasmohq/storage";
export const BACKEND_URL_STORAGE_KEY = "backend_base_url";
export const FALLBACK_BACKEND_BASE_URL = "https://www.surfsense.com";
const storage = new Storage({ area: "local" });
export function normalizeBackendBaseUrl(url: string) {
return url.trim().replace(/\/+$/, "");
}
export const DEFAULT_BACKEND_BASE_URL = normalizeBackendBaseUrl(
process.env.PLASMO_PUBLIC_BACKEND_URL || FALLBACK_BACKEND_BASE_URL
);
export async function getCustomBackendBaseUrl() {
const value = await storage.get(BACKEND_URL_STORAGE_KEY);
return typeof value === "string" ? normalizeBackendBaseUrl(value) : "";
}
export async function setCustomBackendBaseUrl(url: string) {
const normalized = normalizeBackendBaseUrl(url);
if (normalized) {
await storage.set(BACKEND_URL_STORAGE_KEY, normalized);
return normalized;
}
await storage.remove(BACKEND_URL_STORAGE_KEY);
return "";
}
export async function getBackendBaseUrl() {
return (await getCustomBackendBaseUrl()) || DEFAULT_BACKEND_BASE_URL;
}
export async function buildBackendUrl(path: string) {
const baseUrl = await getBackendBaseUrl();
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
return `${baseUrl}${normalizedPath}`;
}

View file

@ -0,0 +1,25 @@
import { NextRequest, NextResponse } from "next/server";
const backendBaseUrl = (process.env.INTERNAL_FASTAPI_BACKEND_URL || "http://backend:8000").replace(
/\/+$/,
""
);
export async function GET(request: NextRequest) {
const response = await fetch(`${backendBaseUrl}/verify-token`, {
method: "GET",
headers: {
Authorization: request.headers.get("authorization") || "",
"X-API-Key": request.headers.get("x-api-key") || "",
},
cache: "no-store",
});
return new NextResponse(response.body, {
status: response.status,
headers: {
"content-type": response.headers.get("content-type") || "application/json",
"cache-control": "no-store",
},
});
}

View file

@ -12,14 +12,20 @@ This guide explains how to run SurfSense using Docker, with options ranging from
Downloads the compose files, generates a `SECRET_KEY`, starts all services, and sets up [Watchtower](https://github.com/nicholas-fedor/watchtower) for automatic daily updates.
<Callout type="info">
Windows users: install [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) first and run the command below in the Ubuntu terminal.
</Callout>
**Prerequisites:** [Docker Desktop](https://www.docker.com/products/docker-desktop/) must be installed and running.
#### For Linux/macOS users:
```bash
curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash
```
#### For Windows users (PowerShell):
```powershell
irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex
```
This creates a `./surfsense/` directory with `docker-compose.yml` and `.env`, then runs `docker compose up -d`.
To skip Watchtower (e.g. in production where you manage updates yourself):