Fix translation keys, session persistence, and add admin tools

- Fix nav_menu translation key: 'Add Webpage(s)' -> 'Add Webpages'
- Fix session persistence: sync baseApiService token on login/logout/load
- Add admin user update script (scripts/update_admin_user.py)
- Add Ollama memory optimization guide

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ojārs Kapteinis 2025-11-18 21:01:28 +02:00
parent ae6455069b
commit 28f2076141
8 changed files with 1055 additions and 4 deletions

515
claude.md
View file

@ -1651,6 +1651,521 @@ For questions or issues related to authentication:
---
## Registration Toggle System
**Implementation Date:** 2025-11-18
**Implemented By:** Claude AI Assistant (Anthropic Sonnet 4.5)
**Feature:** Disable User Registration via Admin Panel
### Overview
Implemented a comprehensive registration control system that allows administrators to disable new user registrations through the admin panel. This provides better control over user access and supports closed registration scenarios for private deployments.
### Implementation Summary
The registration toggle system operates at multiple layers:
1. **Database Layer**: Added `disable_registration` boolean field to `site_configuration` table (migration #39)
2. **Backend API Layer**: Updated `registration_allowed()` dependency to check database toggle and return 403 when disabled
3. **Frontend UI Layer**: Conditionally hide "Sign Up" link on login page based on configuration
4. **Frontend Route Layer**: RouteGuard component protects `/register` route, showing 404 when disabled
5. **Admin Panel Layer**: Added "Registration Control" section to site-settings page for easy toggling
### Backend Changes
#### Database Migration (#39)
**File:** `surfsense_backend/alembic/versions/39_add_disable_registration_to_site_configuration.py`
```python
def upgrade() -> None:
op.add_column(
'site_configuration',
sa.Column('disable_registration', sa.Boolean(), nullable=False, server_default='false')
)
def downgrade() -> None:
op.drop_column('site_configuration', 'disable_registration')
```
**Revision ID:** `'39'` (shortened to fit varchar(32) constraint)
**Down Revision:** `'38_add_site_configuration_table'`
**Default Value:** `false` (registration enabled by default)
#### Database Model
**File:** `surfsense_backend/app/db.py`
```python
class SiteConfiguration(SQLModel, table=True):
# ... existing fields ...
# Registration control
disable_registration = Column(Boolean, nullable=False, default=False)
```
#### API Schemas
**File:** `surfsense_backend/app/schemas/site_configuration.py`
```python
class SiteConfigurationBase(BaseModel):
# ... existing fields ...
# Registration control
disable_registration: bool = False
class SiteConfigurationUpdate(SiteConfigurationBase):
# ... existing fields ...
disable_registration: bool | None = None
```
#### Registration Endpoint Protection
**File:** `surfsense_backend/app/app.py`
```python
async def registration_allowed(session: AsyncSession = Depends(get_async_session)):
# Check environment variable first
if not config.REGISTRATION_ENABLED:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Registration is disabled by system configuration"
)
# Check site configuration database toggle
result = await session.execute(select(SiteConfiguration).where(SiteConfiguration.id == 1))
site_config = result.scalar_one_or_none()
if site_config and site_config.disable_registration:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Registration is currently disabled. Please contact the administrator if you need access."
)
return True
# Applied to registration routes
app.include_router(
fastapi_users.get_register_router(UserRead, UserCreate),
prefix="/auth",
tags=["auth"],
dependencies=[Depends(registration_allowed)], # Enforces registration check
)
```
**Error Response** (when disabled):
```json
{
"detail": "Registration is currently disabled. Please contact the administrator if you need access."
}
```
### Frontend Changes
#### Context/State Management
**File:** `surfsense_web/contexts/SiteConfigContext.tsx`
```typescript
export interface SiteConfig {
// ... existing fields ...
// Registration control
disable_registration: boolean;
}
const defaultConfig: SiteConfig = {
// ... existing fields ...
disable_registration: false,
// ...
};
```
#### Login Form
**File:** `surfsense_web/app/(home)/login/LocalLoginForm.tsx`
**Changes:**
- Added `useSiteConfig()` hook to access site configuration
- Conditionally render "Sign up" link only when `!config.disable_registration`
```typescript
import { useSiteConfig } from "@/contexts/SiteConfigContext";
export function LocalLoginForm() {
const { config } = useSiteConfig();
return (
{/* ... login form ... */}
{!config.disable_registration && (
<div className="mt-4 text-center text-sm">
<p className="text-gray-600 dark:text-gray-400">
{t("dont_have_account")}{" "}
<Link href="/register">
{t("sign_up")}
</Link>
</p>
</div>
)}
);
}
```
#### Route Guard
**File:** `surfsense_web/components/RouteGuard.tsx`
**Changes:**
- Extended `routeKey` type to include `"registration"`
- Added special case handling for registration toggle
```typescript
interface RouteGuardProps {
children: React.ReactNode;
routeKey: "pricing" | "docs" | "contact" | "terms" | "privacy" | "registration";
}
export function RouteGuard({ children, routeKey }: RouteGuardProps) {
const { config, isLoading } = useSiteConfig();
const router = useRouter();
// Special case: registration uses disable_registration instead of disable_registration_route
const disableKey = routeKey === "registration"
? "disable_registration"
: (`disable_${routeKey}_route` as keyof typeof config);
const isDisabled = config[disableKey as keyof typeof config];
if (isDisabled) {
router.replace("/404");
return null;
}
return <>{children}</>;
}
```
#### Register Page
**File:** `surfsense_web/app/(home)/register/page.tsx`
**Changes:**
- Imported `RouteGuard` component
- Wrapped entire page content with `<RouteGuard routeKey="registration">`
```typescript
import { RouteGuard } from "@/components/RouteGuard";
export default function RegisterPage() {
return (
<RouteGuard routeKey="registration">
{/* ... registration form content ... */}
</RouteGuard>
);
}
```
**Behavior:** When `disable_registration` is true, accessing `/register` redirects to `/404`
#### Admin Panel
**File:** `surfsense_web/app/dashboard/site-settings/page.tsx`
**Changes:**
- Added `disable_registration: boolean` to `SiteConfigForm` interface
- Added default value `disable_registration: false` to form state
- Added field to `useEffect` that loads config from API
- Added new "Registration Control" section in UI
```typescript
interface SiteConfigForm {
// ... existing fields ...
disable_registration: boolean;
}
{/* Registration Control Section */}
<section>
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
Registration Control
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
Control user registration availability. When disabled, the Sign Up link will be hidden
and the registration page will show a 404 error. The backend will also block registration
requests with a 403 error.
</p>
<div className="space-y-3">
<ToggleSwitch
label="Disable Registration"
description="Prevent new users from creating accounts"
checked={formData.disable_registration}
onChange={() => handleToggle("disable_registration")}
/>
</div>
</section>
```
### Behavior Summary
#### When Registration is ENABLED (default: `disable_registration = false`):
1. **Login Page (`/login`):**
- Shows "Don't have an account? Sign up" link
- Link navigates to `/register`
2. **Register Page (`/register`):**
- Page loads normally
- Registration form accepts submissions
- Backend POST `/auth/register` succeeds (200)
3. **Admin Panel (`/dashboard/site-settings`):**
- "Disable Registration" toggle is OFF
- Description explains what happens when enabled
#### When Registration is DISABLED (`disable_registration = true`):
1. **Login Page (`/login`):**
- "Don't have an account? Sign up" link is HIDDEN
- No visual indication of registration functionality
2. **Register Page (`/register`):**
- RouteGuard redirects to `/404` (route disabled)
- Page shows standard 404 error
3. **Backend API (`POST /auth/register`):**
- Returns `403 Forbidden`
- Error message: "Registration is currently disabled. Please contact the administrator if you need access."
4. **Admin Panel (`/dashboard/site-settings`):**
- "Disable Registration" toggle is ON (orange)
- Admins can toggle back to enable registration
### Security Considerations
1. **Layered Protection:**
- Frontend hides UI elements (Sign Up link)
- Frontend route guard blocks page access
- Backend API validates and blocks requests
- Defense in depth approach prevents bypass attempts
2. **Database-Driven:**
- Configuration stored in PostgreSQL (singleton row with id=1)
- Atomic updates via SQLAlchemy transactions
- Consistent state across all application instances
3. **Admin-Only Access:**
- Only superusers can access `/dashboard/site-settings`
- JWT authentication required for PUT `/api/v1/site-config`
- Public GET `/api/v1/site-config/public` is read-only
4. **Graceful Error Messages:**
- User-friendly 403 message explains registration is disabled
- Directs users to contact administrator
- No technical details exposed
### Deployment Steps
1. ✅ **Pull latest code:** `git pull origin nightly`
2. ✅ **Apply migration:** `alembic upgrade head` (migration #39)
3. ✅ **Rebuild frontend:** `pnpm build`
4. ✅ **Restart services:** `systemctl restart surfsense.service surfsense-frontend.service surfsense-celery.service surfsense-celery-beat.service`
5. ✅ **Verify deployment:** All services active
**Deployment Date:** 2025-11-18
**Deployment Location:** https://ai.kapteinis.lv
**VPS:** 46.62.230.195
### Migration Notes
#### Issue Encountered: Revision ID Too Long
**Problem:** Alembic `alembic_version` table has `varchar(32)` constraint on `version_num` column. Original revision ID `'39_add_disable_registration_to_site_configuration'` (50 characters) exceeded limit.
**Error:**
```
sqlalchemy.exc.DBAPIError: <class 'asyncpg.exceptions.StringDataRightTruncationError'>:
value too long for type character varying(32)
[SQL: UPDATE alembic_version SET version_num='39_add_disable_registration_to_site_configuration'
WHERE alembic_version.version_num = '38_add_site_configuration_table']
```
**Solution:** Shortened revision ID using sed on VPS:
```bash
sed -i "s/revision: str = '39_add_disable_registration_to_site_configuration'/revision: str = '39'/g" \
alembic/versions/39_add_disable_registration_to_site_configuration.py
```
**Final Revision ID:** `'39'` (2 characters, fits varchar(32))
**Note:** Migration #38 uses long ID `'38_add_site_configuration_table'` but succeeded before (possibly database was modified to allow longer IDs, or varchar constraint was later added).
### Testing Checklist
**Default State (Registration Enabled):**
- [x] Login page shows "Sign up" link
- [x] `/register` page loads successfully
- [x] Registration form accepts submissions
- [x] Backend POST `/auth/register` returns 200/201
- [x] Admin panel toggle is OFF (not orange)
**Toggled State (Registration Disabled):**
- [ ] Admin can toggle "Disable Registration" ON in `/dashboard/site-settings`
- [ ] Changes saved successfully (toast notification)
- [ ] Login page "Sign up" link disappears after refetch
- [ ] `/register` page shows 404 error
- [ ] Backend POST `/auth/register` returns 403 with message
- [ ] Admin panel toggle is ON (orange highlight)
**Toggle Back (Re-enable Registration):**
- [ ] Admin can toggle "Disable Registration" OFF
- [ ] Login page "Sign up" link reappears
- [ ] `/register` page loads normally
- [ ] Backend POST `/auth/register` accepts submissions again
### Files Modified
**Backend (4 files + 1 migration):**
```
surfsense_backend/alembic/versions/39_add_disable_registration_to_site_configuration.py # NEW
surfsense_backend/app/db.py # +3 lines
surfsense_backend/app/app.py # +20 lines
surfsense_backend/app/schemas/site_configuration.py # +4 lines
```
**Frontend (5 files):**
```
surfsense_web/contexts/SiteConfigContext.tsx # +4 lines
surfsense_web/app/(home)/login/LocalLoginForm.tsx # +11 lines, -15 lines (conditional)
surfsense_web/app/(home)/register/page.tsx # +2 lines (import, wrapper)
surfsense_web/components/RouteGuard.tsx # +10 lines (registration support)
surfsense_web/app/dashboard/site-settings/page.tsx # +25 lines (new section)
```
**Total Changes:** 9 files modified, 1 file created, ~80 lines added
### Commit Information
**Commit Hash:** `b8bb74e`
**Branch:** `nightly`
**Message:**
```
Add disable_registration toggle to site configuration
Implemented comprehensive registration control system that allows administrators
to disable new user registrations through the admin panel. This provides better
control over user access and supports closed registration scenarios.
Backend changes:
- Added disable_registration field to SiteConfiguration model (db.py)
- Created migration #39 to add disable_registration column with default false
- Updated SiteConfigurationBase, Update, and Read schemas
- Enhanced registration_allowed() dependency to check database toggle
- Returns 403 with clear message when registration is disabled
Frontend changes:
- Added disable_registration to SiteConfig interface and default config
- Updated LocalLoginForm to conditionally hide "Sign up" link when disabled
- Added RouteGuard support for "registration" route key
- Protected /register page with RouteGuard (shows 404 when disabled)
- Added "Registration Control" section to admin site-settings page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
```
### Use Cases
1. **Private Deployments:**
- Organization wants to control who can create accounts
- Only invite approved users, disable public registration
- Admin manually creates accounts for new employees
2. **Capacity Management:**
- Server approaching resource limits
- Temporarily disable registration until scaling completed
- Re-enable when infrastructure ready
3. **Closed Beta:**
- Limited initial user base for testing
- Disable registration after target number reached
- Re-enable for public launch
4. **Security Incident Response:**
- Spam/abuse accounts being created
- Quickly disable registration to stop attack
- Investigate and re-enable when resolved
### Troubleshooting
#### Issue: Toggle doesn't save in admin panel
**Symptoms:** Clicking toggle works, but changes don't persist after refresh
**Diagnosis:**
1. Check browser console for API errors
2. Verify JWT token is valid (`localStorage.getItem('access_token')`)
3. Check backend logs for 401/403 errors
4. Confirm user is superuser (`is_superuser = true`)
**Solution:** Ensure logged-in user has superuser privileges
#### Issue: Sign Up link still visible after disabling registration
**Symptoms:** Login page shows Sign Up link despite toggle being ON
**Diagnosis:**
1. Check if frontend context refetched after save
2. Inspect SiteConfigContext state in React DevTools
3. Verify GET `/api/v1/site-config/public` returns `disable_registration: true`
**Solution:** Hard refresh browser (Ctrl+Shift+R) or wait for automatic refetch
#### Issue: /register page still loads instead of 404
**Symptoms:** Accessing `/register` shows form instead of 404 error
**Diagnosis:**
1. Verify RouteGuard is wrapping RegisterPage component
2. Check SiteConfigContext has correct `disable_registration` value
3. Confirm RouteGuard logic handles "registration" route key
**Solution:** Verify frontend build includes latest changes, restart frontend service
#### Issue: Backend still accepts registrations despite toggle
**Symptoms:** POST `/auth/register` returns 200 instead of 403
**Diagnosis:**
1. Check database: `SELECT disable_registration FROM site_configuration WHERE id = 1;`
2. Verify `registration_allowed()` function checks database
3. Confirm dependency is applied to register router
**Solution:** Restart backend service to reload code changes
### Future Enhancements
**Potential additions (not yet implemented):**
- [ ] Registration whitelist (allow specific email domains)
- [ ] Invitation-based registration (one-time tokens)
- [ ] Registration queue/approval workflow
- [ ] Auto-disable after N registrations
- [ ] Custom messaging when registration disabled
- [ ] Email notification to admins when disabled
- [ ] Audit log of registration toggle changes
- [ ] Rate limiting for registration attempts
### Support
For questions or issues related to registration control:
- Review admin panel documentation: `/dashboard/site-settings`
- Check backend registration logic: `surfsense_backend/app/app.py:registration_allowed()`
- Examine frontend RouteGuard: `surfsense_web/components/RouteGuard.tsx`
- Verify SiteConfig context: `surfsense_web/contexts/SiteConfigContext.tsx`
- Migration file: `surfsense_backend/alembic/versions/39_add_disable_registration_to_site_configuration.py`
---
## Conclusion
**Overall Security Posture:** ✅ **STRONG**

View file

@ -0,0 +1,383 @@
# Ollama Memory Optimization Guide for SurfSense
This guide helps you optimize Ollama configuration for memory-constrained VPS environments.
## Quick Recommendations
### For VPS with 4GB RAM or less
Use **highly quantized small models**:
```yaml
# global_llm_config.yaml
global_llm_configs:
- id: -1
name: "Ollama Qwen2.5 3B (Q4)"
provider: "OLLAMA"
model_name: "qwen2.5:3b-instruct-q4_K_M"
api_key: ""
api_base: "http://localhost:11434"
language: "English"
litellm_params:
temperature: 0.7
max_tokens: 2048
num_ctx: 2048 # Reduced context window
```
**Recommended models:**
- `qwen2.5:3b-instruct-q4_K_M` (~2GB RAM)
- `gemma2:2b-instruct-q4_K_M` (~1.5GB RAM)
- `phi3:mini-4k-instruct-q4_K_M` (~2GB RAM)
### For VPS with 8GB RAM
Use **medium-sized quantized models**:
```yaml
global_llm_configs:
- id: -1
name: "Ollama Llama 3.1 8B (Q4)"
provider: "OLLAMA"
model_name: "llama3.1:8b-instruct-q4_K_M"
api_key: ""
api_base: "http://localhost:11434"
language: "English"
litellm_params:
temperature: 0.7
max_tokens: 4096
num_ctx: 4096 # Moderate context window
```
**Recommended models:**
- `llama3.1:8b-instruct-q4_K_M` (~5GB RAM)
- `mistral:7b-instruct-q4_K_M` (~4GB RAM)
- `qwen2.5:7b-instruct-q4_K_M` (~4.5GB RAM)
### For VPS with 16GB+ RAM
Use **larger models with better quantization**:
```yaml
global_llm_configs:
- id: -1
name: "Ollama Llama 3.1 8B (Q8)"
provider: "OLLAMA"
model_name: "llama3.1:8b-instruct-q8_0"
api_key: ""
api_base: "http://localhost:11434"
language: "English"
litellm_params:
temperature: 0.7
max_tokens: 8192
num_ctx: 8192
```
---
## Memory Optimization Techniques
### 1. Reduce Context Window Size
The context window (`num_ctx`) directly impacts memory usage. Reduce it for memory savings:
```yaml
litellm_params:
num_ctx: 2048 # Default is often 4096 or 8192
```
**Memory impact:**
- 4096 context: ~2x memory of 2048
- 8192 context: ~4x memory of 2048
### 2. Use Quantized Models
Quantization reduces model precision to save memory:
| Quantization | Quality | Memory | Recommendation |
|-------------|---------|--------|----------------|
| Q8_0 | Highest | 8-bit | Best quality if RAM allows |
| Q5_K_M | High | 5-bit | Good balance |
| Q4_K_M | Medium | 4-bit | **Recommended for VPS** |
| Q3_K_M | Lower | 3-bit | Maximum memory savings |
| Q2_K | Lowest | 2-bit | Not recommended |
### 3. Configure Ollama Environment Variables
Create or edit `/etc/systemd/system/ollama.service.d/override.conf`:
```ini
[Service]
Environment="OLLAMA_NUM_PARALLEL=1"
Environment="OLLAMA_MAX_LOADED_MODELS=1"
Environment="OLLAMA_KEEP_ALIVE=5m"
```
Then reload:
```bash
sudo systemctl daemon-reload
sudo systemctl restart ollama
```
**Environment variables explained:**
- `OLLAMA_NUM_PARALLEL=1`: Process one request at a time (saves memory)
- `OLLAMA_MAX_LOADED_MODELS=1`: Keep only one model in memory
- `OLLAMA_KEEP_ALIVE=5m`: Unload model after 5 minutes of inactivity
### 4. Add Swap Space
If you're running low on memory, add swap:
```bash
# Create 4GB swap file
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# Make permanent
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
# Optimize swappiness
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
```
---
## Model Selection Guide
### Best Models for Memory-Constrained Environments
#### Lightweight (1-3GB RAM)
```bash
ollama pull qwen2.5:3b-instruct-q4_K_M
ollama pull gemma2:2b-instruct-q4_K_M
ollama pull phi3:mini-4k-instruct-q4_K_M
```
#### Medium (4-6GB RAM)
```bash
ollama pull llama3.1:8b-instruct-q4_K_M
ollama pull mistral:7b-instruct-q4_K_M
ollama pull qwen2.5:7b-instruct-q4_K_M
```
#### Quality (8-12GB RAM)
```bash
ollama pull llama3.1:8b-instruct-q8_0
ollama pull qwen2.5:14b-instruct-q4_K_M
```
### Check Available Models
```bash
# List available models
ollama list
# Check model info (including memory requirements)
ollama show llama3.1:8b-instruct-q4_K_M
```
---
## SurfSense Configuration
### Example: Memory-Optimized Setup
Create `surfsense_backend/app/config/global_llm_config.yaml`:
```yaml
# Memory-optimized Ollama configuration for VPS
global_llm_configs:
# Long Context LLM - larger context, fewer tokens
- id: -1
name: "Ollama Long Context (Local)"
provider: "OLLAMA"
model_name: "qwen2.5:7b-instruct-q4_K_M"
api_key: ""
api_base: "http://localhost:11434"
language: "English"
litellm_params:
temperature: 0.7
max_tokens: 2048
num_ctx: 4096
# Fast LLM - smaller model, lower latency
- id: -2
name: "Ollama Fast (Local)"
provider: "OLLAMA"
model_name: "qwen2.5:3b-instruct-q4_K_M"
api_key: ""
api_base: "http://localhost:11434"
language: "English"
litellm_params:
temperature: 0.5
max_tokens: 1024
num_ctx: 2048
# Strategic LLM - same as long context
- id: -3
name: "Ollama Strategic (Local)"
provider: "OLLAMA"
model_name: "qwen2.5:7b-instruct-q4_K_M"
api_key: ""
api_base: "http://localhost:11434"
language: "English"
litellm_params:
temperature: 0.3
max_tokens: 2048
num_ctx: 4096
```
### Hybrid Configuration (Ollama + Cloud)
For best results with limited RAM, use Ollama for fast queries and cloud APIs for complex tasks:
```yaml
global_llm_configs:
# Complex tasks - Cloud API (no local memory needed)
- id: -1
name: "GPT-4 Turbo (Long Context)"
provider: "OPENAI"
model_name: "gpt-4-turbo"
api_key: "sk-your-api-key"
api_base: ""
language: "English"
litellm_params:
temperature: 0.7
max_tokens: 4000
# Quick queries - Local Ollama (fast, private)
- id: -2
name: "Ollama Fast (Local)"
provider: "OLLAMA"
model_name: "qwen2.5:3b-instruct-q4_K_M"
api_key: ""
api_base: "http://localhost:11434"
language: "English"
litellm_params:
temperature: 0.5
max_tokens: 1024
num_ctx: 2048
# Strategic - Cloud API
- id: -3
name: "GPT-4 (Strategic)"
provider: "OPENAI"
model_name: "gpt-4"
api_key: "sk-your-api-key"
api_base: ""
language: "English"
litellm_params:
temperature: 0.3
max_tokens: 2000
```
---
## Troubleshooting
### Error: "Out of memory"
1. **Switch to a smaller model**:
```bash
ollama rm llama3.1:8b
ollama pull qwen2.5:3b-instruct-q4_K_M
```
2. **Reduce context window** in your config:
```yaml
litellm_params:
num_ctx: 2048
```
3. **Free up memory**:
```bash
# Stop unnecessary services
sudo systemctl stop ollama
free -h
sudo systemctl start ollama
```
4. **Check what's using memory**:
```bash
htop
# or
ps aux --sort=-%mem | head -20
```
### Error: "Model not found"
Pull the model first:
```bash
ollama pull qwen2.5:3b-instruct-q4_K_M
```
### Slow responses
1. Ensure only one model is loaded:
```bash
export OLLAMA_MAX_LOADED_MODELS=1
```
2. Add more swap space
3. Use a faster/smaller model
### Connection refused
Ensure Ollama is running:
```bash
sudo systemctl status ollama
sudo systemctl start ollama
```
Check it's listening:
```bash
curl http://localhost:11434/api/tags
```
---
## Monitoring Memory Usage
### Check Ollama memory usage
```bash
# Watch memory in real-time
watch -n 1 'ps aux | grep ollama'
# Check system memory
free -h
# Detailed memory info
cat /proc/meminfo | grep -E "MemTotal|MemFree|MemAvailable|Buffers|Cached"
```
### Log analysis
```bash
# Ollama logs
sudo journalctl -u ollama -f
# System memory pressure
dmesg | grep -i "out of memory"
```
---
## Summary
For a typical VPS with 4-8GB RAM running SurfSense:
1. **Use Q4_K_M quantized models** (best memory/quality balance)
2. **Start with smaller models** (3B-7B parameters)
3. **Reduce context window** to 2048-4096
4. **Configure Ollama** to load only one model at a time
5. **Add swap space** as a safety net
6. **Consider hybrid setup** with cloud APIs for complex tasks
**Recommended starting configuration:**
- Model: `qwen2.5:3b-instruct-q4_K_M` or `qwen2.5:7b-instruct-q4_K_M`
- Context: 2048-4096
- Max tokens: 1024-2048

View file

@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""
Utility script to update admin user password and status.
This script should be run on the VPS after deployment to:
1. Update the admin user's password
2. Ensure is_superuser, is_active, is_verified are all True
Usage:
cd /path/to/SurfSense/surfsense_backend
source venv/bin/activate # or your virtual environment
python scripts/update_admin_user.py
Note: Make sure the .env file is properly configured with DATABASE_URL
"""
import asyncio
import os
import sys
# Add the parent directory to path so we can import app modules
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
from passlib.context import CryptContext
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
# Password hashing context (same as FastAPI-Users uses)
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Configuration
ADMIN_EMAIL = "ojars@kapteinis.lv"
NEW_PASSWORD = "^&U0yXLK1ypZOwLDGFeLT35kCrblITYyAVdVmF3!iJ%kkY1Nl^IS!P"
async def update_admin_user():
"""Update admin user password and status."""
# Get database URL from environment
database_url = os.getenv("DATABASE_URL")
if not database_url:
print("ERROR: DATABASE_URL not found in environment variables")
print("Make sure your .env file is configured correctly")
sys.exit(1)
# Convert postgres:// to postgresql+asyncpg:// if needed
if database_url.startswith("postgres://"):
database_url = database_url.replace("postgres://", "postgresql+asyncpg://", 1)
elif database_url.startswith("postgresql://"):
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
print(f"Connecting to database...")
# Create async engine
engine = create_async_engine(database_url, echo=False)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
try:
# Hash the new password
hashed_password = pwd_context.hash(NEW_PASSWORD)
# Update the user
result = await session.execute(
update(UserTable)
.where(UserTable.email == ADMIN_EMAIL)
.values(
hashed_password=hashed_password,
is_superuser=True,
is_active=True,
is_verified=True
)
.returning(UserTable.id, UserTable.email)
)
updated_user = result.first()
if updated_user:
await session.commit()
print(f"\n✅ Successfully updated user: {ADMIN_EMAIL}")
print(f" - User ID: {updated_user.id}")
print(f" - Password: Updated to new value")
print(f" - is_superuser: True")
print(f" - is_active: True")
print(f" - is_verified: True")
else:
print(f"\n❌ User not found: {ADMIN_EMAIL}")
print(" Make sure the email address is correct")
# List existing users for debugging
from sqlalchemy import text
users_result = await session.execute(
text("SELECT email FROM \"user\" LIMIT 10")
)
users = users_result.fetchall()
if users:
print("\n Existing users in database:")
for user in users:
print(f" - {user[0]}")
except Exception as e:
print(f"\n❌ Error updating user: {e}")
await session.rollback()
raise
finally:
await engine.dispose()
# Import the User model after environment is loaded
# We need to do this here to ensure DATABASE_URL is available
if __name__ == "__main__":
print("=" * 50)
print("Admin User Update Script")
print("=" * 50)
print(f"\nTarget user: {ADMIN_EMAIL}")
print(f"New password: {'*' * 10} (hidden)")
print()
# Now import the User model
try:
from app.db import User as UserTable
except ImportError:
# Try alternative import if running from different directory
try:
from surfsense_backend.app.db import User as UserTable
except ImportError:
print("ERROR: Could not import User model")
print("Make sure you're running this script from the surfsense_backend directory")
sys.exit(1)
# Run the async update
asyncio.run(update_admin_user())
print("\n" + "=" * 50)
print("Done!")
print("=" * 50)

View file

@ -50,7 +50,7 @@ export default function DashboardLayout({
url: `/dashboard/${search_space_id}/sources/add`,
},
{
title: "Add Webpage(s)",
title: "Add Webpages",
url: `/dashboard/${search_space_id}/documents/webpage`,
},
{

View file

@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { AnnouncementBanner } from "@/components/announcement-banner";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { baseApiService } from "@/lib/apis/base-api.service";
interface DashboardLayoutProps {
children: React.ReactNode;
@ -21,6 +22,9 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
router.push("/login");
return;
}
// Ensure the baseApiService has the correct token
// This handles cases where the page is refreshed or navigated to directly
baseApiService.setBearerToken(token);
setIsCheckingAuth(false);
}, [router]);

View file

@ -67,7 +67,7 @@ export default function SiteSettingsPage() {
const checkSuperuser = async () => {
try {
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
const token = localStorage.getItem("access_token");
const token = localStorage.getItem("surfsense_bearer_token");
if (!token) {
router.push("/login");
@ -150,7 +150,7 @@ export default function SiteSettingsPage() {
setIsSaving(true);
try {
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
const token = localStorage.getItem("access_token");
const token = localStorage.getItem("surfsense_bearer_token");
const response = await fetch(`${backendUrl}/api/v1/site-config`, {
method: "PUT",

View file

@ -2,6 +2,7 @@
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { baseApiService } from "@/lib/apis/base-api.service";
interface TokenHandlerProps {
redirectPath?: string; // Path to redirect after storing token
@ -35,7 +36,10 @@ const TokenHandler = ({
try {
// Store token in localStorage
localStorage.setItem(storageKey, token);
// console.log(`Token stored in localStorage with key: ${storageKey}`);
// Update the baseApiService singleton with the new token
// This ensures subsequent API calls use the correct token
baseApiService.setBearerToken(token);
// Redirect to specified path
router.push(redirectPath);

View file

@ -3,6 +3,7 @@
import { BadgeCheck, LogOut, Settings } from "lucide-react";
import { useRouter } from "next/navigation";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { baseApiService } from "@/lib/apis/base-api.service";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@ -29,6 +30,8 @@ export function UserDropdown({
try {
if (typeof window !== "undefined") {
localStorage.removeItem("surfsense_bearer_token");
// Clear the baseApiService token to prevent stale auth state
baseApiService.setBearerToken("");
router.push("/");
}
} catch (error) {