543 lines
21 KiB
Markdown
543 lines
21 KiB
Markdown
# Sojorn Admin Panel — Comprehensive System Documentation
|
|
|
|
> Last updated: February 6, 2026
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Overview](#overview)
|
|
2. [Architecture](#architecture)
|
|
3. [Authentication & Security](#authentication--security)
|
|
4. [Server Deployment](#server-deployment)
|
|
5. [Frontend (Next.js)](#frontend-nextjs)
|
|
6. [Backend API Routes](#backend-api-routes)
|
|
7. [Database Schema](#database-schema)
|
|
8. [Feature Reference](#feature-reference)
|
|
9. [Environment Variables](#environment-variables)
|
|
10. [Troubleshooting](#troubleshooting)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
The Sojorn Admin Panel is an internal tool for platform administrators to manage users, moderate content, review appeals, configure the feed algorithm, and monitor system health. It is a standalone Next.js 14 application that communicates with the existing Sojorn Go backend via a dedicated set of admin API endpoints.
|
|
|
|
**Key characteristics:**
|
|
- Separate frontend deployment from the main Flutter app
|
|
- Role-based access — only users with `role = 'admin'` in the `profiles` table can log in
|
|
- All admin actions are logged to the `audit_log` table
|
|
- Invisible Cloudflare Turnstile bot protection on login
|
|
- JWT authentication with 24-hour token expiry
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
┌─────────────────────┐ HTTPS ┌──────────────────────┐
|
|
│ Browser │ ◄────────────► │ Nginx │
|
|
│ admin.sojorn.net │ │ (reverse proxy) │
|
|
└─────────────────────┘ └──────┬───────────────┘
|
|
│
|
|
┌──────────────────┼──────────────────┐
|
|
│ port 3002 │ port 8080 │
|
|
▼ ▼ │
|
|
┌─────────────────┐ ┌─────────────────┐ │
|
|
│ Next.js 14 │ │ Go Backend │ │
|
|
│ (sojorn-admin) │ │ (sojorn-api) │ │
|
|
│ SSR + Static │ │ Gin framework │ │
|
|
└─────────────────┘ └────────┬────────┘ │
|
|
│ │
|
|
┌────────▼────────┐ │
|
|
│ PostgreSQL │ │
|
|
│ sojorn database │ │
|
|
└─────────────────┘ │
|
|
│
|
|
┌─────────────────┐ │
|
|
│ Cloudflare R2 │───────┘
|
|
│ (media storage) │
|
|
└─────────────────┘
|
|
```
|
|
|
|
### Tech Stack
|
|
|
|
| Component | Technology |
|
|
|-----------|-----------|
|
|
| Frontend | Next.js 14, TypeScript, TailwindCSS, Recharts, Lucide icons |
|
|
| Backend | Go 1.21+, Gin framework, pgx (PostgreSQL driver) |
|
|
| Database | PostgreSQL 15+ |
|
|
| Process management | systemd |
|
|
| Reverse proxy | Nginx |
|
|
| Bot protection | Cloudflare Turnstile (invisible mode) |
|
|
| Auth | JWT (HS256), bcrypt password hashing |
|
|
|
|
---
|
|
|
|
## Authentication & Security
|
|
|
|
### Login Flow
|
|
|
|
1. User enters email + password on `/login`
|
|
2. Cloudflare Turnstile invisible widget generates a token in the background
|
|
3. Frontend sends `POST /api/v1/admin/login` with `{ email, password, turnstile_token }`
|
|
4. Backend verifies Turnstile token with Cloudflare API
|
|
5. Backend checks `users` table for valid credentials (bcrypt)
|
|
6. Backend verifies account status is `active`
|
|
7. Backend checks `profiles.role = 'admin'`
|
|
8. Returns JWT (`access_token`) with 24-hour expiry + user profile data
|
|
9. Token stored in `localStorage` as `admin_token`
|
|
|
|
### Middleware Chain (Protected Routes)
|
|
|
|
All `/api/v1/admin/*` routes (except `/login`) pass through:
|
|
|
|
1. **AuthMiddleware** — Validates JWT, extracts `user_id` from `sub` claim, sets it in Gin context
|
|
2. **AdminMiddleware** — Queries `profiles.role` for the authenticated user, rejects non-admin users with 403
|
|
|
|
### Security Measures
|
|
|
|
- **Invisible Turnstile** — Blocks automated login attacks without user friction
|
|
- **Graceful degradation** — If `TURNSTILE_SECRET` is empty, verification is skipped (dev mode)
|
|
- **bcrypt** — Passwords hashed with bcrypt (default cost)
|
|
- **JWT expiry** — Admin tokens expire after 24 hours (vs 7 days for regular users)
|
|
- **Auto-logout** — Frontend redirects to `/login` on any 401 response
|
|
- **Audit logging** — Status changes, post deletions, and moderation actions are logged
|
|
|
|
### Granting Admin Access
|
|
|
|
```sql
|
|
UPDATE profiles SET role = 'admin' WHERE handle = 'your_handle';
|
|
```
|
|
|
|
Valid roles: `user`, `moderator`, `admin`
|
|
|
|
---
|
|
|
|
## Server Deployment
|
|
|
|
### Services
|
|
|
|
| Service | systemd unit | Port | Binary/Entry |
|
|
|---------|-------------|------|-------------|
|
|
| Go API | `sojorn-api` | 8080 | `/opt/sojorn/bin/api` |
|
|
| Admin Frontend | `sojorn-admin` | 3002 | `node .../next start --port 3002` |
|
|
|
|
### File Locations on Server
|
|
|
|
```
|
|
/opt/sojorn/
|
|
├── .env # Shared environment variables
|
|
├── bin/
|
|
│ └── api # Compiled Go binary
|
|
├── go-backend/ # Go source code
|
|
│ ├── cmd/api/main.go
|
|
│ ├── internal/
|
|
│ │ ├── handlers/admin_handler.go
|
|
│ │ ├── middleware/admin.go
|
|
│ │ └── ...
|
|
│ └── .env # Symlink or copy of /opt/sojorn/.env
|
|
├── admin/ # Next.js admin frontend
|
|
│ ├── .env.local # Frontend env vars
|
|
│ ├── .next/ # Build output
|
|
│ ├── node_modules/
|
|
│ ├── src/
|
|
│ │ ├── app/ # Page routes
|
|
│ │ └── lib/ # API client, auth context
|
|
│ └── package.json
|
|
└── firebase-service-account.json
|
|
```
|
|
|
|
### systemd Service Files
|
|
|
|
**`/etc/systemd/system/sojorn-admin.service`**:
|
|
```ini
|
|
[Unit]
|
|
Description=Sojorn Admin Panel
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=patrick
|
|
Group=patrick
|
|
WorkingDirectory=/opt/sojorn/admin
|
|
ExecStart=/usr/bin/node /opt/sojorn/admin/node_modules/next/dist/bin/next start --port 3002
|
|
Restart=on-failure
|
|
RestartSec=30
|
|
StartLimitIntervalSec=120
|
|
StartLimitBurst=3
|
|
Environment=NODE_ENV=production
|
|
Environment=NEXT_PUBLIC_API_URL=https://api.sojorn.net
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
|
|
### Nginx Configuration
|
|
|
|
**`/etc/nginx/sites-available/sojorn-admin`**:
|
|
```nginx
|
|
server {
|
|
listen 80;
|
|
server_name admin.sojorn.net;
|
|
|
|
location / {
|
|
proxy_pass http://127.0.0.1:3002;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection 'upgrade';
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_cache_bypass $http_upgrade;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Common Operations
|
|
|
|
```bash
|
|
# Rebuild and deploy Go backend
|
|
cd /opt/sojorn/go-backend
|
|
go build -ldflags='-s -w' -o /opt/sojorn/bin/api ./cmd/api/main.go
|
|
sudo systemctl restart sojorn-api
|
|
|
|
# Rebuild and deploy admin frontend
|
|
cd /opt/sojorn/admin
|
|
npx next build
|
|
sudo systemctl restart sojorn-admin
|
|
|
|
# View logs
|
|
sudo journalctl -u sojorn-admin -f
|
|
sudo journalctl -u sojorn-api -f
|
|
|
|
# Check service status
|
|
sudo systemctl status sojorn-admin
|
|
sudo systemctl status sojorn-api
|
|
|
|
# SSL certificate (after DNS A record is pointed)
|
|
sudo certbot --nginx -d admin.sojorn.net
|
|
```
|
|
|
|
---
|
|
|
|
## Frontend (Next.js)
|
|
|
|
### Pages
|
|
|
|
| Route | File | Description |
|
|
|-------|------|-------------|
|
|
| `/` | `app/page.tsx` | Dashboard with stats cards and growth charts |
|
|
| `/login` | `app/login/page.tsx` | Admin login with invisible Turnstile |
|
|
| `/users` | `app/users/page.tsx` | User list with search, filter by status/role |
|
|
| `/users/[id]` | `app/users/[id]/page.tsx` | User detail — profile, stats, admin actions |
|
|
| `/posts` | `app/posts/page.tsx` | Post list with search, filter by status |
|
|
| `/posts/[id]` | `app/posts/[id]/page.tsx` | Post detail — content, media, moderation flags, admin actions |
|
|
| `/moderation` | `app/moderation/page.tsx` | Moderation queue — AI-flagged content pending review |
|
|
| `/appeals` | `app/appeals/page.tsx` | User appeals against violations |
|
|
| `/reports` | `app/reports/page.tsx` | User-submitted reports |
|
|
| `/algorithm` | `app/algorithm/page.tsx` | Feed algorithm & moderation threshold tuning |
|
|
| `/categories` | `app/categories/page.tsx` | Category management — create, edit, toggle sensitive |
|
|
| `/system` | `app/system/page.tsx` | System health, DB stats, audit log |
|
|
| `/settings` | `app/settings/page.tsx` | Admin session info, API URL override |
|
|
|
|
### Key Libraries
|
|
|
|
| Library | Source |
|
|
|---------|--------|
|
|
| `api.ts` | `src/lib/api.ts` — Singleton API client with JWT token management |
|
|
| `auth.tsx` | `src/lib/auth.tsx` — React context provider for auth state |
|
|
|
|
### API Client (`src/lib/api.ts`)
|
|
|
|
The `ApiClient` class provides typed methods for every admin API endpoint. Key behaviors:
|
|
|
|
- **Token management** — Stored in `localStorage` as `admin_token`, attached as `Bearer` header
|
|
- **Auto-logout** — Any 401 response clears token and redirects to `/login`
|
|
- **Base URL** — Configurable via `NEXT_PUBLIC_API_URL` environment variable
|
|
- **Error handling** — Throws `Error` with server-provided error message
|
|
|
|
### Auth Context (`src/lib/auth.tsx`)
|
|
|
|
- Wraps app in `AuthProvider`
|
|
- On mount, validates existing token by calling `getDashboardStats()`
|
|
- Provides `login()`, `logout()`, `isAuthenticated`, `isLoading`, `user`
|
|
|
|
---
|
|
|
|
## Backend API Routes
|
|
|
|
All routes prefixed with `/api/v1/admin/`.
|
|
|
|
### Authentication (no middleware)
|
|
|
|
| Method | Path | Handler | Description |
|
|
|--------|------|---------|-------------|
|
|
| `POST` | `/login` | `AdminLogin` | Email/password login with Turnstile verification |
|
|
|
|
### Dashboard (requires auth + admin)
|
|
|
|
| Method | Path | Handler | Description |
|
|
|--------|------|---------|-------------|
|
|
| `GET` | `/dashboard` | `GetDashboardStats` | User/post/moderation/appeal/report counts |
|
|
| `GET` | `/growth?days=30` | `GetGrowthStats` | Daily user & post creation counts for charts |
|
|
|
|
### User Management
|
|
|
|
| Method | Path | Handler | Description |
|
|
|--------|------|---------|-------------|
|
|
| `GET` | `/users?limit=50&offset=0&search=&status=&role=` | `ListUsers` | Paginated user list with filters |
|
|
| `GET` | `/users/:id` | `GetUser` | Full user profile with follower/post/violation counts |
|
|
| `PATCH` | `/users/:id/status` | `UpdateUserStatus` | Set status: `active`, `suspended`, `banned`, `deactivated` |
|
|
| `PATCH` | `/users/:id/role` | `UpdateUserRole` | Set role: `user`, `moderator`, `admin` |
|
|
| `PATCH` | `/users/:id/verification` | `UpdateUserVerification` | Toggle `is_official` and `is_verified` flags |
|
|
| `POST` | `/users/:id/reset-strikes` | `ResetUserStrikes` | Reset violation strike counter to 0 |
|
|
|
|
### Post Management
|
|
|
|
| Method | Path | Handler | Description |
|
|
|--------|------|---------|-------------|
|
|
| `GET` | `/posts?limit=50&offset=0&search=&status=&author_id=` | `ListPosts` | Paginated post list with filters |
|
|
| `GET` | `/posts/:id` | `GetPost` | Full post detail with moderation flags |
|
|
| `PATCH` | `/posts/:id/status` | `UpdatePostStatus` | Set status: `active`, `flagged`, `removed` |
|
|
| `DELETE` | `/posts/:id` | `DeletePost` | Soft-delete (sets `deleted_at` + status `removed`) |
|
|
|
|
### Moderation Queue
|
|
|
|
| Method | Path | Handler | Description |
|
|
|--------|------|---------|-------------|
|
|
| `GET` | `/moderation?limit=50&offset=0&status=pending` | `GetModerationQueue` | AI-flagged content awaiting review |
|
|
| `PATCH` | `/moderation/:id/review` | `ReviewModerationFlag` | Actions: `approve`, `dismiss`, `remove_content`, `ban_user` |
|
|
|
|
### Appeals
|
|
|
|
| Method | Path | Handler | Description |
|
|
|--------|------|---------|-------------|
|
|
| `GET` | `/appeals?limit=50&offset=0&status=pending` | `ListAppeals` | User appeals with violation details |
|
|
| `PATCH` | `/appeals/:id/review` | `ReviewAppeal` | Decision: `approved` or `rejected`, optional content restore |
|
|
|
|
### Reports
|
|
|
|
| Method | Path | Handler | Description |
|
|
|--------|------|---------|-------------|
|
|
| `GET` | `/reports?limit=50&offset=0&status=pending` | `ListReports` | User-submitted reports |
|
|
| `PATCH` | `/reports/:id` | `UpdateReportStatus` | Set status: `reviewed`, `dismissed`, `actioned` |
|
|
|
|
### Algorithm & Feed Config
|
|
|
|
| Method | Path | Handler | Description |
|
|
|--------|------|---------|-------------|
|
|
| `GET` | `/algorithm` | `GetAlgorithmConfig` | All key-value config pairs |
|
|
| `PUT` | `/algorithm` | `UpdateAlgorithmConfig` | Upsert a config `{ key, value }` |
|
|
|
|
### Categories
|
|
|
|
| Method | Path | Handler | Description |
|
|
|--------|------|---------|-------------|
|
|
| `GET` | `/categories` | `ListCategories` | All content categories |
|
|
| `POST` | `/categories` | `CreateCategory` | Create `{ slug, name, description?, is_sensitive? }` |
|
|
| `PATCH` | `/categories/:id` | `UpdateCategory` | Update name, description, or sensitive flag |
|
|
|
|
### System
|
|
|
|
| Method | Path | Handler | Description |
|
|
|--------|------|---------|-------------|
|
|
| `GET` | `/health` | `GetSystemHealth` | DB ping, latency, connection pool stats, DB size |
|
|
| `GET` | `/audit-log?limit=50&offset=0` | `GetAuditLog` | Admin action history with actor handles |
|
|
|
|
---
|
|
|
|
## Database Schema
|
|
|
|
### Tables Created by Admin Migration
|
|
|
|
**`algorithm_config`** — Key-value store for feed and moderation tuning:
|
|
|
|
| Column | Type | Description |
|
|
|--------|------|-------------|
|
|
| `key` | `TEXT PRIMARY KEY` | Config identifier |
|
|
| `value` | `TEXT NOT NULL` | Config value |
|
|
| `description` | `TEXT` | Human-readable description |
|
|
| `updated_at` | `TIMESTAMPTZ` | Last modification time |
|
|
|
|
Default seed values:
|
|
|
|
| Key | Default | Description |
|
|
|-----|---------|-------------|
|
|
| `feed_recency_weight` | `0.4` | Weight for post recency in feed ranking |
|
|
| `feed_engagement_weight` | `0.3` | Weight for engagement metrics |
|
|
| `feed_harmony_weight` | `0.2` | Weight for author harmony/trust score |
|
|
| `feed_diversity_weight` | `0.1` | Weight for content diversity |
|
|
| `moderation_auto_flag_threshold` | `0.7` | AI score threshold for auto-flagging |
|
|
| `moderation_auto_remove_threshold` | `0.95` | AI score threshold for auto-removal |
|
|
| `moderation_greed_keyword_threshold` | `0.7` | Spam/greed detection threshold |
|
|
| `feed_max_posts_per_author` | `3` | Max posts from same author per feed page |
|
|
| `feed_boost_mutual_follow` | `1.5` | Boost multiplier for mutual follows |
|
|
| `feed_beacon_boost` | `1.2` | Boost multiplier for beacon posts |
|
|
|
|
**`audit_log`** — Admin action history:
|
|
|
|
| Column | Type | Description |
|
|
|--------|------|-------------|
|
|
| `id` | `UUID PRIMARY KEY` | Unique entry ID |
|
|
| `actor_id` | `UUID` | Admin who performed the action |
|
|
| `action` | `TEXT NOT NULL` | Action type (e.g., `post_status_change`, `admin_delete_post`) |
|
|
| `target_type` | `TEXT NOT NULL` | Entity type: `user`, `post`, `comment`, `appeal`, `report`, `config` |
|
|
| `target_id` | `UUID` | ID of the affected entity |
|
|
| `details` | `TEXT` | JSON string with action-specific metadata |
|
|
| `created_at` | `TIMESTAMPTZ` | Timestamp |
|
|
|
|
### Columns Ensured by Migration
|
|
|
|
The migration ensures these columns exist (added if missing):
|
|
|
|
| Table | Column | Type | Default |
|
|
|-------|--------|------|---------|
|
|
| `profiles` | `role` | `TEXT` | `'user'` |
|
|
| `profiles` | `is_verified` | `BOOLEAN` | `FALSE` |
|
|
| `profiles` | `is_private` | `BOOLEAN` | `FALSE` |
|
|
| `users` | `status` | `TEXT` | `'active'` |
|
|
| `users` | `last_login` | `TIMESTAMPTZ` | `NULL` |
|
|
|
|
### Pre-existing Tables Used by Admin
|
|
|
|
| Table | Admin Usage |
|
|
|-------|-------------|
|
|
| `users` | Login validation, status management, growth stats |
|
|
| `profiles` | Role checks, user details, verification flags |
|
|
| `posts` | Content listing, status changes, deletion |
|
|
| `comments` | Moderation flag targets, appeal content restoration |
|
|
| `moderation_flags` | Moderation queue — AI-generated flags with scores |
|
|
| `user_violations` | Violation records linked to moderation flags |
|
|
| `user_appeals` | Appeal records linked to violations |
|
|
| `user_status_history` | Log of admin-initiated status changes |
|
|
| `reports` | User-submitted reports of other users/content |
|
|
| `categories` | Content categories managed by admins |
|
|
| `follows` | Follower/following counts on user detail page |
|
|
|
|
---
|
|
|
|
## Feature Reference
|
|
|
|
### Dashboard
|
|
|
|
Displays real-time aggregate stats:
|
|
- **Users**: total, active, suspended, banned, new today
|
|
- **Posts**: total, active, flagged, removed, new today
|
|
- **Moderation**: pending flags, reviewed flags
|
|
- **Appeals**: pending, approved, rejected
|
|
- **Reports**: pending count
|
|
- **Growth charts**: Daily user & post registrations (configurable 7/30/90 day window)
|
|
|
|
### Moderation Workflow
|
|
|
|
1. **AI flagging** — Posts/comments are automatically analyzed by the `ModerationService` using OpenAI + Google Vision
|
|
2. **Three Poisons Score** — Content is scored on Hate, Greed, Delusion dimensions
|
|
3. **Auto-flag** — Content exceeding `moderation_auto_flag_threshold` is flagged for review
|
|
4. **Admin review** — Admin sees flagged content in the moderation queue with scores
|
|
5. **Actions available**:
|
|
- **Approve** — Content is fine, dismiss the flag
|
|
- **Dismiss** — Same as approve (flag was a false positive)
|
|
- **Remove content** — Soft-delete the post/comment
|
|
- **Ban user** — Ban the author and action the flag
|
|
|
|
### Appeal Workflow
|
|
|
|
1. User receives a violation (triggered by moderation)
|
|
2. User submits an appeal with reason and context
|
|
3. Appeal appears in admin panel with violation details, AI scores, and original content
|
|
4. Admin reviews and decides:
|
|
- **Approve** — Optionally restore the removed content
|
|
- **Reject** — Violation stands, include written reasoning (min 5 chars)
|
|
|
|
### User Management Actions
|
|
|
|
- **Change status**: `active` ↔ `suspended` ↔ `banned` ↔ `deactivated` (requires reason)
|
|
- **Change role**: `user` ↔ `moderator` ↔ `admin`
|
|
- **Toggle verification**: `is_official` and `is_verified` badges
|
|
- **Reset strikes**: Clear the violation counter
|
|
|
|
---
|
|
|
|
## Environment Variables
|
|
|
|
### Frontend (`/opt/sojorn/admin/.env.local`)
|
|
|
|
| Variable | Value | Description |
|
|
|----------|-------|-------------|
|
|
| `NEXT_PUBLIC_API_URL` | `https://api.sojorn.net` | Go backend base URL |
|
|
| `NEXT_PUBLIC_TURNSTILE_SITE_KEY` | `0x4AAAAAAC...` | Cloudflare Turnstile site key (invisible mode) |
|
|
|
|
### Backend (`/opt/sojorn/.env`)
|
|
|
|
The admin system uses these existing environment variables:
|
|
|
|
| Variable | Description |
|
|
|----------|-------------|
|
|
| `DATABASE_URL` | PostgreSQL connection string |
|
|
| `JWT_SECRET` | Secret for signing/verifying JWT tokens |
|
|
| `TURNSTILE_SECRET` | Cloudflare Turnstile server-side verification key |
|
|
| `PORT` | API server port (default: `8080`) |
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Admin login returns "Admin access required"
|
|
|
|
The user's profile doesn't have `role = 'admin'`. Fix:
|
|
```sql
|
|
UPDATE profiles SET role = 'admin' WHERE handle = 'your_handle';
|
|
```
|
|
|
|
### Admin login returns "Invalid credentials"
|
|
|
|
- Verify the email matches what's in the `users` table
|
|
- Password is validated against `encrypted_password` via bcrypt
|
|
- Check the user's `status` is `active` (not `pending`, `suspended`, or `banned`)
|
|
|
|
### Admin login returns "Security verification failed"
|
|
|
|
Cloudflare Turnstile rejected the request. Possible causes:
|
|
- `TURNSTILE_SECRET` in backend `.env` doesn't match the site key in frontend `.env.local`
|
|
- The Turnstile widget hostname doesn't include `admin.sojorn.net` in Cloudflare dashboard
|
|
- Bot or automated request without a valid Turnstile token
|
|
|
|
### sojorn-admin service won't start
|
|
|
|
```bash
|
|
# Check logs
|
|
sudo journalctl -u sojorn-admin -n 50 --no-pager
|
|
|
|
# Check if port 3002 is in use
|
|
ss -tlnp | grep 3002
|
|
|
|
# If port is taken, find and kill the process
|
|
sudo fuser -k 3002/tcp
|
|
sudo systemctl restart sojorn-admin
|
|
```
|
|
|
|
### sojorn-api panics on startup
|
|
|
|
Check for duplicate route registrations in `cmd/api/main.go`. The Go backend will panic if two routes resolve to the same path (e.g., legacy routes conflicting with admin group routes).
|
|
|
|
### Frontend shows "Loading..." indefinitely
|
|
|
|
- Check browser console for network errors
|
|
- Verify `NEXT_PUBLIC_API_URL` in `.env.local` is correct and reachable
|
|
- Ensure the Go API is running: `sudo systemctl status sojorn-api`
|
|
- Check CORS — the backend must allow the admin domain in `CORS_ORIGINS`
|
|
|
|
### Database migration errors
|
|
|
|
Run the migration manually:
|
|
```bash
|
|
export PGPASSWORD=your_db_password
|
|
psql -U postgres -h localhost -d sojorn -f /opt/sojorn/go-backend/internal/database/migrations/20260206000001_admin_panel_tables.up.sql
|
|
```
|
|
|
|
### PM2 conflicts
|
|
|
|
Port 3001 is used by another PM2-managed site on this server. The admin panel uses port **3002** to avoid conflicts. Do not change it to 3001.
|