mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-10 20:35:17 +02:00
Merge branch 'MODSetter:dev' into dev
This commit is contained in:
commit
4bfcd91f3f
17 changed files with 977 additions and 47 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,3 +5,4 @@ node_modules/
|
|||
.ruff_cache/
|
||||
.venv
|
||||
.pnpm-store
|
||||
.DS_Store
|
||||
|
|
|
|||
11
README.es.md
11
README.es.md
|
|
@ -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/).
|
||||
|
|
|
|||
11
README.hi.md
11
README.hi.md
|
|
@ -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/) देखें।
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -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/).
|
||||
|
|
|
|||
|
|
@ -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/).
|
||||
|
|
|
|||
|
|
@ -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
350
docker/scripts/install.ps1
Normal 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."
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
343
docker/scripts/migrate-database.ps1
Normal file
343
docker/scripts/migrate-database.ps1
Normal 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)"
|
||||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
41
surfsense_browser_extension/utils/backend-url.ts
Normal file
41
surfsense_browser_extension/utils/backend-url.ts
Normal 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}`;
|
||||
}
|
||||
25
surfsense_web/app/verify-token/route.ts
Normal file
25
surfsense_web/app/verify-token/route.ts
Normal 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",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue