feat: Add container restart functionality from web UI

- Added restart button to settings page
- Created /api/restart endpoint (file-flag based)
- Implemented watch-restart.sh daemon
- Added systemd service for restart watcher
- Updated README with restart setup instructions
- Container automatically restarts when settings changed

Settings flow:
1. User edits settings in web UI
2. Click 'Save Settings' to persist to .env
3. Click 'Restart Bot' to apply changes
4. Watcher detects flag and restarts container
5. New settings loaded automatically
This commit is contained in:
mindesbunister
2025-10-24 15:06:26 +02:00
parent 9e0d9b88f9
commit 26864c10f2
7 changed files with 174 additions and 10 deletions

View File

@@ -46,14 +46,15 @@ RUN npm run build
# ================================ # ================================
FROM node:20-alpine AS runner FROM node:20-alpine AS runner
# Install dumb-init for proper signal handling # Install dumb-init for proper signal handling and Docker CLI for restart capability
RUN apk add --no-cache dumb-init RUN apk add --no-cache dumb-init docker-cli
WORKDIR /app WORKDIR /app
# Create non-root user # Create non-root user
RUN addgroup --system --gid 1001 nodejs && \ RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs adduser --system --uid 1001 nextjs && \
addgroup nextjs root
# Copy necessary files from builder # Copy necessary files from builder
COPY --from=builder /app/next.config.js ./ COPY --from=builder /app/next.config.js ./

View File

@@ -178,6 +178,24 @@ POST /api/trading/check-risk
# Get current settings # Get current settings
GET /api/settings GET /api/settings
# Update settings
POST /api/settings
{
"MAX_POSITION_SIZE_USD": 100,
"LEVERAGE": 10,
"STOP_LOSS_PERCENT": -1.5,
...
}
# Restart bot container (apply settings)
POST /api/restart
```
**Notes:**
- Settings changes require container restart to take effect
- Use the web UI's "Restart Bot" button or call `/api/restart`
- Restart watcher must be running (see setup below)
# Update settings # Update settings
POST /api/settings POST /api/settings
{ {
@@ -223,6 +241,26 @@ docker compose restart trading-bot
docker compose down docker compose down
``` ```
### Restart Watcher (Required for Web UI Restart Button)
The restart watcher monitors for restart requests from the web UI:
```bash
# Start watcher manually
cd /home/icke/traderv4
nohup ./watch-restart.sh > logs/restart-watcher.log 2>&1 &
# OR install as systemd service (recommended)
sudo cp trading-bot-restart-watcher.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable trading-bot-restart-watcher
sudo systemctl start trading-bot-restart-watcher
# Check watcher status
sudo systemctl status trading-bot-restart-watcher
```
The watcher enables the "Restart Bot" button in the web UI to automatically restart the container when settings are changed.
### Environment Variables ### Environment Variables
All settings are configured via `.env` file: All settings are configured via `.env` file:
- Drift wallet credentials - Drift wallet credentials

38
app/api/restart/route.ts Normal file
View File

@@ -0,0 +1,38 @@
/**
* Container Restart API Endpoint
*
* Creates a restart flag file that triggers container restart from the host
*/
import { NextResponse } from 'next/server'
import fs from 'fs'
import path from 'path'
const RESTART_FLAG = path.join(process.cwd(), 'logs', '.restart-requested')
export async function POST() {
try {
// Create logs directory if it doesn't exist
const logsDir = path.dirname(RESTART_FLAG)
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true })
}
// Create restart flag file
fs.writeFileSync(RESTART_FLAG, new Date().toISOString(), 'utf-8')
return NextResponse.json({
success: true,
message: 'Restart requested. Container will restart in a few seconds...'
})
} catch (error: any) {
console.error('Failed to create restart flag:', error)
return NextResponse.json(
{
error: 'Failed to request restart',
details: error.message
},
{ status: 500 }
)
}
}

View File

@@ -29,6 +29,7 @@ export default function SettingsPage() {
const [settings, setSettings] = useState<TradingSettings | null>(null) const [settings, setSettings] = useState<TradingSettings | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [restarting, setRestarting] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null) const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
useEffect(() => { useEffect(() => {
@@ -58,7 +59,7 @@ export default function SettingsPage() {
}) })
if (response.ok) { if (response.ok) {
setMessage({ type: 'success', text: 'Settings saved! Restart the bot to apply changes.' }) setMessage({ type: 'success', text: 'Settings saved! Click "Restart Bot" to apply changes.' })
} else { } else {
setMessage({ type: 'error', text: 'Failed to save settings' }) setMessage({ type: 'error', text: 'Failed to save settings' })
} }
@@ -68,6 +69,25 @@ export default function SettingsPage() {
setSaving(false) setSaving(false)
} }
const restartBot = async () => {
setRestarting(true)
setMessage(null)
try {
const response = await fetch('/api/restart', {
method: 'POST',
})
if (response.ok) {
setMessage({ type: 'success', text: 'Bot is restarting... Settings will be applied in ~10 seconds.' })
} else {
setMessage({ type: 'error', text: 'Failed to restart bot. Please restart manually with: docker restart trading-bot' })
}
} catch (error) {
setMessage({ type: 'error', text: 'Failed to restart bot. Please restart manually with: docker restart trading-bot' })
}
setRestarting(false)
}
const updateSetting = (key: keyof TradingSettings, value: any) => { const updateSetting = (key: keyof TradingSettings, value: any) => {
if (!settings) return if (!settings) return
setSettings({ ...settings, [key]: value }) setSettings({ ...settings, [key]: value })
@@ -301,7 +321,7 @@ export default function SettingsPage() {
</Section> </Section>
</div> </div>
{/* Save Button */} {/* Action Buttons */}
<div className="mt-8 flex gap-4"> <div className="mt-8 flex gap-4">
<button <button
onClick={saveSettings} onClick={saveSettings}
@@ -310,16 +330,23 @@ export default function SettingsPage() {
> >
{saving ? '💾 Saving...' : '💾 Save Settings'} {saving ? '💾 Saving...' : '💾 Save Settings'}
</button> </button>
<button
onClick={restartBot}
disabled={restarting}
className="flex-1 bg-gradient-to-r from-green-500 to-emerald-500 text-white font-bold py-4 px-6 rounded-lg hover:from-green-600 hover:to-emerald-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{restarting ? '🔄 Restarting...' : '🔄 Restart Bot'}
</button>
<button <button
onClick={loadSettings} onClick={loadSettings}
className="bg-slate-700 text-white font-bold py-4 px-6 rounded-lg hover:bg-slate-600 transition-all" className="bg-slate-700 text-white font-bold py-4 px-6 rounded-lg hover:bg-slate-600 transition-all"
> >
🔄 Reset Reset
</button> </button>
</div> </div>
<div className="mt-4 text-center text-slate-500 text-sm"> <div className="mt-4 text-center text-slate-400 text-sm">
Changes require bot restart to take effect 💡 Save settings first, then click Restart Bot to apply changes
</div> </div>
</div> </div>
</div> </div>

View File

@@ -20,10 +20,10 @@ services:
# Load from .env file (create from .env.example) # Load from .env file (create from .env.example)
DRIFT_WALLET_PRIVATE_KEY: ${DRIFT_WALLET_PRIVATE_KEY} DRIFT_WALLET_PRIVATE_KEY: ${DRIFT_WALLET_PRIVATE_KEY}
DRIFT_ENV: ${DRIFT_ENV:-mainnet-beta} DRIFT_ENV: "${DRIFT_ENV:-mainnet-beta}"
API_SECRET_KEY: ${API_SECRET_KEY} API_SECRET_KEY: ${API_SECRET_KEY}
SOLANA_RPC_URL: ${SOLANA_RPC_URL} SOLANA_RPC_URL: ${SOLANA_RPC_URL}
PYTH_HERMES_URL: ${PYTH_HERMES_URL:-https://hermes.pyth.network} PYTH_HERMES_URL: "${PYTH_HERMES_URL:-https://hermes.pyth.network}"
# Trading configuration # Trading configuration
MAX_POSITION_SIZE_USD: ${MAX_POSITION_SIZE_USD:-50} MAX_POSITION_SIZE_USD: ${MAX_POSITION_SIZE_USD:-50}
@@ -52,6 +52,9 @@ services:
# Mount logs directory # Mount logs directory
- ./logs:/app/logs - ./logs:/app/logs
# Mount Docker socket for container restart capability
- /var/run/docker.sock:/var/run/docker.sock
# Mount for hot reload in development (comment out in production) # Mount for hot reload in development (comment out in production)
# - ./v4:/app/v4:ro # - ./v4:/app/v4:ro

View File

@@ -0,0 +1,17 @@
[Unit]
Description=Trading Bot v4 Container Restart Watcher
After=docker.service
Requires=docker.service
[Service]
Type=simple
User=root
WorkingDirectory=/home/icke/traderv4
ExecStart=/home/icke/traderv4/watch-restart.sh
Restart=always
RestartSec=10
StandardOutput=append:/home/icke/traderv4/logs/restart-watcher.log
StandardError=append:/home/icke/traderv4/logs/restart-watcher.log
[Install]
WantedBy=multi-user.target

40
watch-restart.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/bin/bash
#
# Container Restart Watcher
# Monitors for restart flag and restarts the trading-bot container
#
WATCH_DIR="/home/icke/traderv4/logs"
RESTART_FLAG="$WATCH_DIR/.restart-requested"
CONTAINER_NAME="trading-bot-v4"
echo "🔍 Watching for restart requests in: $WATCH_DIR"
echo "📦 Container: $CONTAINER_NAME"
echo ""
# Create logs directory if it doesn't exist
mkdir -p "$WATCH_DIR"
while true; do
if [ -f "$RESTART_FLAG" ]; then
echo "🔄 Restart requested at $(cat $RESTART_FLAG)"
echo "🔄 Restarting container: $CONTAINER_NAME"
# Remove flag before restart
rm "$RESTART_FLAG"
# Restart container
cd /home/icke/traderv4
docker compose restart $CONTAINER_NAME
if [ $? -eq 0 ]; then
echo "✅ Container restarted successfully"
else
echo "❌ Failed to restart container"
fi
echo ""
fi
# Check every 2 seconds
sleep 2
done