Maintenance mode activates after current draw completes

- When admin enables maintenance, it's set to 'pending' state
- Maintenance activates automatically after the current draw completes
- Admin can use immediate=true to force immediate activation
- Frontend shows 'Maintenance Scheduled' banner when pending
- Telegram bot warns users but still allows purchases when pending
- Both mode and pending status tracked in system_settings table
This commit is contained in:
Michilis
2025-12-09 00:46:55 +00:00
parent 0eb8a6c580
commit 404fdf2610
12 changed files with 474 additions and 4 deletions

View File

@@ -189,3 +189,122 @@ export async function retryPayout(req: Request, res: Response) {
}
}
/**
* GET /admin/maintenance
* Get current maintenance mode status
*/
export async function getMaintenanceStatus(req: Request, res: Response) {
try {
const result = await db.query(
`SELECT value FROM system_settings WHERE key = 'maintenance_mode'`
);
const pendingResult = await db.query(
`SELECT value FROM system_settings WHERE key = 'maintenance_pending'`
);
const messageResult = await db.query(
`SELECT value FROM system_settings WHERE key = 'maintenance_message'`
);
const enabled = result.rows[0]?.value === 'true';
const pending = pendingResult.rows[0]?.value === 'true';
const message = messageResult.rows[0]?.value || 'System is under maintenance. Please try again later.';
return res.json({
version: '1.0',
data: {
maintenance_mode: enabled,
maintenance_pending: pending,
message: message,
},
});
} catch (error: any) {
console.error('Get maintenance status error:', error);
return res.status(500).json({
version: '1.0',
error: 'INTERNAL_ERROR',
message: 'Failed to get maintenance status',
});
}
}
/**
* POST /admin/maintenance
* Enable or disable maintenance mode
* When enabling, maintenance is set to "pending" and activates after current draw completes
*/
export async function setMaintenanceMode(req: Request, res: Response) {
try {
const { enabled, message, immediate } = req.body;
if (typeof enabled !== 'boolean') {
return res.status(400).json({
version: '1.0',
error: 'INVALID_INPUT',
message: 'enabled must be a boolean',
});
}
if (enabled) {
if (immediate === true) {
// Immediate activation (admin override)
await db.query(
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_mode', 'true', datetime('now'))
ON CONFLICT(key) DO UPDATE SET value = 'true', updated_at = datetime('now')`
);
await db.query(
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_pending', 'false', datetime('now'))
ON CONFLICT(key) DO UPDATE SET value = 'false', updated_at = datetime('now')`
);
console.log('Maintenance mode ENABLED IMMEDIATELY');
} else {
// Set pending - will activate after current draw completes
await db.query(
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_pending', 'true', datetime('now'))
ON CONFLICT(key) DO UPDATE SET value = 'true', updated_at = datetime('now')`
);
console.log('Maintenance mode PENDING (will activate after current draw)');
}
} else {
// Disable both active and pending maintenance
await db.query(
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_mode', 'false', datetime('now'))
ON CONFLICT(key) DO UPDATE SET value = 'false', updated_at = datetime('now')`
);
await db.query(
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_pending', 'false', datetime('now'))
ON CONFLICT(key) DO UPDATE SET value = 'false', updated_at = datetime('now')`
);
console.log('Maintenance mode DISABLED');
}
// Update message if provided
if (message && typeof message === 'string') {
await db.query(
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_message', $1, datetime('now'))
ON CONFLICT(key) DO UPDATE SET value = $1, updated_at = datetime('now')`,
[message]
);
}
// Get current state
const modeResult = await db.query(`SELECT value FROM system_settings WHERE key = 'maintenance_mode'`);
const pendingResult = await db.query(`SELECT value FROM system_settings WHERE key = 'maintenance_pending'`);
return res.json({
version: '1.0',
data: {
maintenance_mode: modeResult.rows[0]?.value === 'true',
maintenance_pending: pendingResult.rows[0]?.value === 'true',
message: message || 'System is under maintenance. Please try again later.',
},
});
} catch (error: any) {
console.error('Set maintenance mode error:', error);
return res.status(500).json({
version: '1.0',
error: 'INTERNAL_ERROR',
message: 'Failed to set maintenance mode',
});
}
}

View File

@@ -8,6 +8,68 @@ import config from '../config';
import { JackpotCycle, TicketPurchase, Ticket, Payout } from '../types';
import { AuthRequest } from '../middleware/auth';
/**
* GET /status/maintenance
* Public endpoint to check maintenance mode
*/
export async function getPublicMaintenanceStatus(req: Request, res: Response) {
try {
const result = await db.query(
`SELECT value FROM system_settings WHERE key = 'maintenance_mode'`
);
const pendingResult = await db.query(
`SELECT value FROM system_settings WHERE key = 'maintenance_pending'`
);
const messageResult = await db.query(
`SELECT value FROM system_settings WHERE key = 'maintenance_message'`
);
const enabled = result.rows[0]?.value === 'true';
const pending = pendingResult.rows[0]?.value === 'true';
const message = messageResult.rows[0]?.value || 'System is under maintenance. Please try again later.';
return res.json({
version: '1.0',
data: {
maintenance_mode: enabled,
maintenance_pending: pending,
message: enabled ? message : (pending ? 'Maintenance will begin after the current draw completes.' : null),
},
});
} catch (error: any) {
// If table doesn't exist yet, return not in maintenance
return res.json({
version: '1.0',
data: {
maintenance_mode: false,
maintenance_pending: false,
message: null,
},
});
}
}
/**
* Helper to check if system is in maintenance mode
*/
async function isMaintenanceMode(): Promise<{ enabled: boolean; message: string }> {
try {
const result = await db.query(
`SELECT value FROM system_settings WHERE key = 'maintenance_mode'`
);
const messageResult = await db.query(
`SELECT value FROM system_settings WHERE key = 'maintenance_message'`
);
return {
enabled: result.rows[0]?.value === 'true',
message: messageResult.rows[0]?.value || 'System is under maintenance. Please try again later.',
};
} catch {
return { enabled: false, message: '' };
}
}
const toIsoString = (value: any): string => {
if (!value) {
return new Date().toISOString();
@@ -140,6 +202,16 @@ export async function getNextJackpot(req: Request, res: Response) {
*/
export async function buyTickets(req: AuthRequest, res: Response) {
try {
// Check maintenance mode first
const maintenance = await isMaintenanceMode();
if (maintenance.enabled) {
return res.status(503).json({
version: '1.0',
error: 'MAINTENANCE_MODE',
message: maintenance.message,
});
}
const { tickets, lightning_address, nostr_pubkey, name, buyer_name } = req.body;
const userId = req.user?.id || null;
const authNostrPubkey = req.user?.nostr_pubkey || null;

View File

@@ -158,6 +158,17 @@ class DatabaseWrapper {
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS system_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Initialize default settings
INSERT OR IGNORE INTO system_settings (key, value) VALUES ('maintenance_mode', 'false');
INSERT OR IGNORE INTO system_settings (key, value) VALUES ('maintenance_pending', 'false');
INSERT OR IGNORE INTO system_settings (key, value) VALUES ('maintenance_message', 'System is under maintenance. Please try again later.');
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_cycles_status_time ON jackpot_cycles(status, scheduled_at);
CREATE INDEX IF NOT EXISTS idx_ticketpurchase_paymenthash ON ticket_purchases(lnbits_payment_hash);

View File

@@ -3,7 +3,9 @@ import {
listCycles,
runDrawManually,
retryPayout,
listPayouts
listPayouts,
getMaintenanceStatus,
setMaintenanceMode
} from '../controllers/admin';
import { verifyAdmin } from '../middleware/auth';
@@ -126,5 +128,78 @@ router.get('/payouts', listPayouts);
*/
router.post('/payouts/:id/retry', retryPayout);
/**
* @swagger
* /admin/maintenance:
* get:
* summary: Get maintenance mode status
* tags: [Admin]
* security:
* - adminKey: []
* responses:
* 200:
* description: Maintenance status
* content:
* application/json:
* schema:
* type: object
* properties:
* maintenance_mode:
* type: boolean
* message:
* type: string
* 403:
* description: Invalid admin key
*/
router.get('/maintenance', getMaintenanceStatus);
/**
* @swagger
* /admin/maintenance:
* post:
* summary: Enable or disable maintenance mode
* description: When enabling, maintenance is set to "pending" and activates after current draw completes. Use immediate=true to activate immediately.
* tags: [Admin]
* security:
* - adminKey: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - enabled
* properties:
* enabled:
* type: boolean
* description: Whether to enable maintenance mode
* message:
* type: string
* description: Custom maintenance message
* immediate:
* type: boolean
* description: If true, activate immediately instead of waiting for draw to complete
* responses:
* 200:
* description: Maintenance mode updated
* content:
* application/json:
* schema:
* type: object
* properties:
* maintenance_mode:
* type: boolean
* maintenance_pending:
* type: boolean
* message:
* type: string
* 400:
* description: Invalid input
* 403:
* description: Invalid admin key
*/
router.post('/maintenance', setMaintenanceMode);
export default router;

View File

@@ -3,7 +3,8 @@ import {
getNextJackpot,
buyTickets,
getTicketStatus,
getPastWins
getPastWins,
getPublicMaintenanceStatus
} from '../controllers/public';
import { buyRateLimiter, ticketStatusRateLimiter } from '../middleware/rateLimit';
import { optionalAuth } from '../middleware/auth';
@@ -187,5 +188,32 @@ router.get('/jackpot/past-wins', getPastWins);
*/
router.get('/tickets/:id', ticketStatusRateLimiter, getTicketStatus);
/**
* @swagger
* /status/maintenance:
* get:
* summary: Check if system is in maintenance mode
* tags: [Public]
* responses:
* 200:
* description: Maintenance status
* content:
* application/json:
* schema:
* type: object
* properties:
* version:
* type: string
* data:
* type: object
* properties:
* maintenance_mode:
* type: boolean
* message:
* type: string
* nullable: true
*/
router.get('/status/maintenance', getPublicMaintenanceStatus);
export default router;

View File

@@ -244,6 +244,8 @@ async function checkAndExecuteDraws(): Promise<void> {
await executeDraw(cycle.id);
// Cancel unpaid invoices for this cycle after draw
await cancelUnpaidPurchases(cycle.id);
// Check if maintenance was pending and activate it
await activatePendingMaintenance();
}
} catch (error) {
@@ -275,6 +277,33 @@ async function cancelUnpaidPurchases(cycleId: string): Promise<void> {
}
}
/**
* Check if maintenance is pending and activate it after draw completion
*/
async function activatePendingMaintenance(): Promise<void> {
try {
const pendingResult = await db.query(
`SELECT value FROM system_settings WHERE key = 'maintenance_pending'`
);
if (pendingResult.rows[0]?.value === 'true') {
// Activate maintenance mode
await db.query(
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_mode', 'true', datetime('now'))
ON CONFLICT(key) DO UPDATE SET value = 'true', updated_at = datetime('now')`
);
// Clear pending flag
await db.query(
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_pending', 'false', datetime('now'))
ON CONFLICT(key) DO UPDATE SET value = 'false', updated_at = datetime('now')`
);
console.log('🔧 MAINTENANCE MODE ACTIVATED (pending maintenance enabled after draw)');
}
} catch (error) {
console.error('Error activating pending maintenance:', error);
}
}
/**
* Update cycles to 'sales_open' status when appropriate
*/