mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-05 06:23:22 +00:00
added server.js for frontend when not using nginx in deployment
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "settings" ADD COLUMN "last_update_check" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "latest_version" TEXT,
|
||||||
|
ADD COLUMN "update_available" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -187,6 +187,9 @@ model Settings {
|
|||||||
githubRepoUrl String @map("github_repo_url") @default("git@github.com:9technologygroup/patchmon.net.git") // GitHub repository URL for version checking
|
githubRepoUrl String @map("github_repo_url") @default("git@github.com:9technologygroup/patchmon.net.git") // GitHub repository URL for version checking
|
||||||
repositoryType String @map("repository_type") @default("public") // "public" or "private"
|
repositoryType String @map("repository_type") @default("public") // "public" or "private"
|
||||||
sshKeyPath String? @map("ssh_key_path") // Optional SSH key path for deploy key authentication
|
sshKeyPath String? @map("ssh_key_path") // Optional SSH key path for deploy key authentication
|
||||||
|
lastUpdateCheck DateTime? @map("last_update_check") // When the system last checked for updates
|
||||||
|
updateAvailable Boolean @map("update_available") @default(false) // Whether an update is available
|
||||||
|
latestVersion String? @map("latest_version") // Latest available version
|
||||||
createdAt DateTime @map("created_at") @default(now())
|
createdAt DateTime @map("created_at") @default(now())
|
||||||
updatedAt DateTime @map("updated_at") @updatedAt
|
updatedAt DateTime @map("updated_at") @updatedAt
|
||||||
|
|
||||||
|
|||||||
@@ -142,202 +142,36 @@ router.post('/test-ssh-key', authenticateToken, requireManageSettings, async (re
|
|||||||
// Check for updates from GitHub
|
// Check for updates from GitHub
|
||||||
router.get('/check-updates', authenticateToken, requireManageSettings, async (req, res) => {
|
router.get('/check-updates', authenticateToken, requireManageSettings, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Get GitHub repo URL from settings
|
// Get cached update information from settings
|
||||||
const settings = await prisma.settings.findFirst();
|
const settings = await prisma.settings.findFirst();
|
||||||
console.log('Settings retrieved for version check:', {
|
|
||||||
id: settings?.id,
|
if (!settings) {
|
||||||
githubRepoUrl: settings?.githubRepoUrl,
|
return res.status(400).json({ error: 'Settings not found' });
|
||||||
repositoryType: settings?.repositoryType
|
}
|
||||||
|
|
||||||
|
const currentVersion = '1.2.4';
|
||||||
|
const latestVersion = settings.latestVersion || currentVersion;
|
||||||
|
const isUpdateAvailable = settings.updateAvailable || false;
|
||||||
|
const lastUpdateCheck = settings.lastUpdateCheck;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
currentVersion,
|
||||||
|
latestVersion,
|
||||||
|
isUpdateAvailable,
|
||||||
|
lastUpdateCheck,
|
||||||
|
repositoryType: settings.repositoryType || 'public',
|
||||||
|
latestRelease: {
|
||||||
|
tagName: latestVersion ? `v${latestVersion}` : null,
|
||||||
|
version: latestVersion,
|
||||||
|
repository: settings.githubRepoUrl ? settings.githubRepoUrl.split('/').slice(-2).join('/') : null,
|
||||||
|
accessMethod: settings.repositoryType === 'private' ? 'ssh' : 'api'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!settings || !settings.githubRepoUrl) {
|
|
||||||
return res.status(400).json({ error: 'GitHub repository URL not configured' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract owner and repo from GitHub URL
|
} catch (error) {
|
||||||
// Support both SSH and HTTPS formats:
|
console.error('Error getting update information:', error);
|
||||||
// git@github.com:owner/repo.git
|
res.status(500).json({ error: 'Failed to get update information' });
|
||||||
// https://github.com/owner/repo.git
|
}
|
||||||
const repoUrl = settings.githubRepoUrl;
|
|
||||||
console.log('Using repository URL:', repoUrl);
|
|
||||||
let owner, repo;
|
|
||||||
|
|
||||||
if (repoUrl.includes('git@github.com:')) {
|
|
||||||
const match = repoUrl.match(/git@github\.com:([^\/]+)\/([^\/]+)\.git/);
|
|
||||||
if (match) {
|
|
||||||
[, owner, repo] = match;
|
|
||||||
}
|
|
||||||
} else if (repoUrl.includes('github.com/')) {
|
|
||||||
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/);
|
|
||||||
if (match) {
|
|
||||||
[, owner, repo] = match;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Extracted owner and repo:', { owner, repo });
|
|
||||||
|
|
||||||
if (!owner || !repo) {
|
|
||||||
return res.status(400).json({ error: 'Invalid GitHub repository URL format' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine repository type and set up appropriate access method
|
|
||||||
const repositoryType = settings.repositoryType || 'public';
|
|
||||||
const isPrivate = repositoryType === 'private';
|
|
||||||
|
|
||||||
let latestTag;
|
|
||||||
|
|
||||||
if (isPrivate) {
|
|
||||||
// Use SSH with deploy keys for private repositories
|
|
||||||
const sshRepoUrl = `git@github.com:${owner}/${repo}.git`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let sshKeyPath = null;
|
|
||||||
|
|
||||||
// First, try to use the configured SSH key path from settings
|
|
||||||
if (settings.sshKeyPath) {
|
|
||||||
try {
|
|
||||||
require('fs').accessSync(settings.sshKeyPath);
|
|
||||||
sshKeyPath = settings.sshKeyPath;
|
|
||||||
console.log(`Using configured SSH key at: ${sshKeyPath}`);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Configured SSH key path not accessible: ${settings.sshKeyPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no configured path or it's not accessible, try common locations
|
|
||||||
if (!sshKeyPath) {
|
|
||||||
const possibleKeyPaths = [
|
|
||||||
'/root/.ssh/id_ed25519', // Root user (if service runs as root)
|
|
||||||
'/root/.ssh/id_rsa', // Root user RSA key
|
|
||||||
'/home/patchmon/.ssh/id_ed25519', // PatchMon user
|
|
||||||
'/home/patchmon/.ssh/id_rsa', // PatchMon user RSA key
|
|
||||||
'/var/www/.ssh/id_ed25519', // Web user
|
|
||||||
'/var/www/.ssh/id_rsa' // Web user RSA key
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const path of possibleKeyPaths) {
|
|
||||||
try {
|
|
||||||
require('fs').accessSync(path);
|
|
||||||
sshKeyPath = path;
|
|
||||||
console.log(`Found SSH key at: ${path}`);
|
|
||||||
break;
|
|
||||||
} catch (e) {
|
|
||||||
// Key not found at this path, try next
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sshKeyPath) {
|
|
||||||
throw new Error('No SSH deploy key found. Please configure the SSH key path in settings or ensure a deploy key is installed in one of the expected locations.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const env = {
|
|
||||||
...process.env,
|
|
||||||
GIT_SSH_COMMAND: `ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no -o IdentitiesOnly=yes`
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch the latest tag using SSH with deploy key
|
|
||||||
const { stdout: sshLatestTag } = await execAsync(
|
|
||||||
`git ls-remote --tags --sort=-version:refname ${sshRepoUrl} | head -n 1 | sed 's/.*refs\\/tags\\///' | sed 's/\\^{}//'`,
|
|
||||||
{
|
|
||||||
timeout: 10000,
|
|
||||||
env: env
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
latestTag = sshLatestTag;
|
|
||||||
} catch (sshError) {
|
|
||||||
console.error('SSH Git error:', sshError.message);
|
|
||||||
throw sshError;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Use GitHub API for public repositories (no authentication required)
|
|
||||||
try {
|
|
||||||
const httpsRepoUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
|
|
||||||
|
|
||||||
const response = await fetch(httpsRepoUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/vnd.github.v3+json',
|
|
||||||
'User-Agent': 'PatchMon-Server/1.2.4'
|
|
||||||
},
|
|
||||||
timeout: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const releaseData = await response.json();
|
|
||||||
latestTag = releaseData.tag_name;
|
|
||||||
} catch (apiError) {
|
|
||||||
console.error('GitHub API error:', apiError.message);
|
|
||||||
throw apiError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestVersion = latestTag.trim().replace('v', ''); // Remove 'v' prefix
|
|
||||||
const currentVersion = '1.2.4';
|
|
||||||
|
|
||||||
// Simple version comparison (assumes semantic versioning)
|
|
||||||
const isUpdateAvailable = compareVersions(latestVersion, currentVersion) > 0;
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
currentVersion,
|
|
||||||
latestVersion,
|
|
||||||
isUpdateAvailable,
|
|
||||||
repositoryType,
|
|
||||||
latestRelease: {
|
|
||||||
tagName: latestTag.trim(),
|
|
||||||
version: latestVersion,
|
|
||||||
repository: `${owner}/${repo}`,
|
|
||||||
accessMethod: isPrivate ? 'ssh' : 'api',
|
|
||||||
...(isPrivate && { sshUrl: `git@github.com:${owner}/${repo}.git`, sshKeyUsed: settings.sshKeyPath })
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Version check error:', error.message);
|
|
||||||
|
|
||||||
if (error.message.includes('Permission denied') || error.message.includes('Host key verification failed')) {
|
|
||||||
return res.status(403).json({
|
|
||||||
error: 'Access denied to repository',
|
|
||||||
suggestion: isPrivate
|
|
||||||
? 'Ensure your deploy key is properly configured and has access to the repository. Check that the key has read access to the repository.'
|
|
||||||
: 'Check that the repository URL is correct and the repository is public.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.message.includes('not found') || error.message.includes('does not exist')) {
|
|
||||||
return res.status(404).json({
|
|
||||||
error: 'Repository not found',
|
|
||||||
suggestion: 'Check that the repository URL is correct and accessible.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.message.includes('No SSH deploy key found')) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: 'No SSH deploy key found',
|
|
||||||
suggestion: 'Please install a deploy key in one of the expected locations: /root/.ssh/, /home/patchmon/.ssh/, or /var/www/.ssh/'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.message.includes('GitHub API error')) {
|
|
||||||
return res.status(500).json({
|
|
||||||
error: 'Failed to fetch repository information via GitHub API',
|
|
||||||
details: error.message,
|
|
||||||
suggestion: 'Check that the repository URL is correct and the repository is public.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(500).json({
|
|
||||||
error: 'Failed to fetch repository information',
|
|
||||||
details: error.message,
|
|
||||||
suggestion: isPrivate
|
|
||||||
? 'Check deploy key configuration and repository access permissions.'
|
|
||||||
: 'Check repository URL and ensure it is accessible.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simple version comparison function
|
// Simple version comparison function
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const dashboardPreferencesRoutes = require('./routes/dashboardPreferencesRoutes'
|
|||||||
const repositoryRoutes = require('./routes/repositoryRoutes');
|
const repositoryRoutes = require('./routes/repositoryRoutes');
|
||||||
const versionRoutes = require('./routes/versionRoutes');
|
const versionRoutes = require('./routes/versionRoutes');
|
||||||
const tfaRoutes = require('./routes/tfaRoutes');
|
const tfaRoutes = require('./routes/tfaRoutes');
|
||||||
|
const updateScheduler = require('./services/updateScheduler');
|
||||||
|
|
||||||
// Initialize Prisma client
|
// Initialize Prisma client
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
@@ -160,6 +161,7 @@ process.on('SIGTERM', async () => {
|
|||||||
if (process.env.ENABLE_LOGGING === 'true') {
|
if (process.env.ENABLE_LOGGING === 'true') {
|
||||||
logger.info('SIGTERM received, shutting down gracefully');
|
logger.info('SIGTERM received, shutting down gracefully');
|
||||||
}
|
}
|
||||||
|
updateScheduler.stop();
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
@@ -168,6 +170,7 @@ process.on('SIGINT', async () => {
|
|||||||
if (process.env.ENABLE_LOGGING === 'true') {
|
if (process.env.ENABLE_LOGGING === 'true') {
|
||||||
logger.info('SIGINT received, shutting down gracefully');
|
logger.info('SIGINT received, shutting down gracefully');
|
||||||
}
|
}
|
||||||
|
updateScheduler.stop();
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
@@ -178,6 +181,9 @@ app.listen(PORT, () => {
|
|||||||
logger.info(`Server running on port ${PORT}`);
|
logger.info(`Server running on port ${PORT}`);
|
||||||
logger.info(`Environment: ${process.env.NODE_ENV}`);
|
logger.info(`Environment: ${process.env.NODE_ENV}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start update scheduler
|
||||||
|
updateScheduler.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
247
backend/src/services/updateScheduler.js
Normal file
247
backend/src/services/updateScheduler.js
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
const { PrismaClient } = require('@prisma/client');
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
const { promisify } = require('util');
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
class UpdateScheduler {
|
||||||
|
constructor() {
|
||||||
|
this.isRunning = false;
|
||||||
|
this.intervalId = null;
|
||||||
|
this.checkInterval = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the scheduler
|
||||||
|
start() {
|
||||||
|
if (this.isRunning) {
|
||||||
|
console.log('Update scheduler is already running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔄 Starting update scheduler...');
|
||||||
|
this.isRunning = true;
|
||||||
|
|
||||||
|
// Run initial check
|
||||||
|
this.checkForUpdates();
|
||||||
|
|
||||||
|
// Schedule regular checks
|
||||||
|
this.intervalId = setInterval(() => {
|
||||||
|
this.checkForUpdates();
|
||||||
|
}, this.checkInterval);
|
||||||
|
|
||||||
|
console.log(`✅ Update scheduler started - checking every ${this.checkInterval / (60 * 60 * 1000)} hours`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the scheduler
|
||||||
|
stop() {
|
||||||
|
if (!this.isRunning) {
|
||||||
|
console.log('Update scheduler is not running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🛑 Stopping update scheduler...');
|
||||||
|
this.isRunning = false;
|
||||||
|
|
||||||
|
if (this.intervalId) {
|
||||||
|
clearInterval(this.intervalId);
|
||||||
|
this.intervalId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Update scheduler stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for updates
|
||||||
|
async checkForUpdates() {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Checking for updates...');
|
||||||
|
|
||||||
|
// Get settings
|
||||||
|
const settings = await prisma.settings.findFirst();
|
||||||
|
if (!settings || !settings.githubRepoUrl) {
|
||||||
|
console.log('⚠️ No GitHub repository configured, skipping update check');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract owner and repo from GitHub URL
|
||||||
|
const repoUrl = settings.githubRepoUrl;
|
||||||
|
let owner, repo;
|
||||||
|
|
||||||
|
if (repoUrl.includes('git@github.com:')) {
|
||||||
|
const match = repoUrl.match(/git@github\.com:([^\/]+)\/([^\/]+)\.git/);
|
||||||
|
if (match) {
|
||||||
|
[, owner, repo] = match;
|
||||||
|
}
|
||||||
|
} else if (repoUrl.includes('github.com/')) {
|
||||||
|
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+?)(?:\.git)?$/);
|
||||||
|
if (match) {
|
||||||
|
[, owner, repo] = match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!owner || !repo) {
|
||||||
|
console.log('⚠️ Could not parse GitHub repository URL, skipping update check');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let latestVersion;
|
||||||
|
const isPrivate = settings.repositoryType === 'private';
|
||||||
|
|
||||||
|
if (isPrivate) {
|
||||||
|
// Use SSH for private repositories
|
||||||
|
latestVersion = await this.checkPrivateRepo(settings, owner, repo);
|
||||||
|
} else {
|
||||||
|
// Use GitHub API for public repositories
|
||||||
|
latestVersion = await this.checkPublicRepo(owner, repo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!latestVersion) {
|
||||||
|
console.log('⚠️ Could not determine latest version, skipping update check');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentVersion = '1.2.4';
|
||||||
|
const isUpdateAvailable = this.compareVersions(latestVersion, currentVersion) > 0;
|
||||||
|
|
||||||
|
// Update settings with check results
|
||||||
|
await prisma.settings.update({
|
||||||
|
where: { id: settings.id },
|
||||||
|
data: {
|
||||||
|
lastUpdateCheck: new Date(),
|
||||||
|
updateAvailable: isUpdateAvailable,
|
||||||
|
latestVersion: latestVersion
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Update check completed - Current: ${currentVersion}, Latest: ${latestVersion}, Update Available: ${isUpdateAvailable}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error checking for updates:', error.message);
|
||||||
|
|
||||||
|
// Update last check time even on error
|
||||||
|
try {
|
||||||
|
const settings = await prisma.settings.findFirst();
|
||||||
|
if (settings) {
|
||||||
|
await prisma.settings.update({
|
||||||
|
where: { id: settings.id },
|
||||||
|
data: {
|
||||||
|
lastUpdateCheck: new Date(),
|
||||||
|
updateAvailable: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (updateError) {
|
||||||
|
console.error('❌ Error updating last check time:', updateError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check private repository using SSH
|
||||||
|
async checkPrivateRepo(settings, owner, repo) {
|
||||||
|
try {
|
||||||
|
let sshKeyPath = settings.sshKeyPath;
|
||||||
|
|
||||||
|
// Try to find SSH key if not configured
|
||||||
|
if (!sshKeyPath) {
|
||||||
|
const possibleKeyPaths = [
|
||||||
|
'/root/.ssh/id_ed25519',
|
||||||
|
'/root/.ssh/id_rsa',
|
||||||
|
'/home/patchmon/.ssh/id_ed25519',
|
||||||
|
'/home/patchmon/.ssh/id_rsa',
|
||||||
|
'/var/www/.ssh/id_ed25519',
|
||||||
|
'/var/www/.ssh/id_rsa'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const path of possibleKeyPaths) {
|
||||||
|
try {
|
||||||
|
require('fs').accessSync(path);
|
||||||
|
sshKeyPath = path;
|
||||||
|
break;
|
||||||
|
} catch (e) {
|
||||||
|
// Key not found at this path, try next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sshKeyPath) {
|
||||||
|
throw new Error('No SSH deploy key found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sshRepoUrl = `git@github.com:${owner}/${repo}.git`;
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
GIT_SSH_COMMAND: `ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no -o IdentitiesOnly=yes`
|
||||||
|
};
|
||||||
|
|
||||||
|
const { stdout: sshLatestTag } = await execAsync(
|
||||||
|
`git ls-remote --tags --sort=-version:refname ${sshRepoUrl} | head -n 1 | sed 's/.*refs\\/tags\\///' | sed 's/\\^{}//'`,
|
||||||
|
{
|
||||||
|
timeout: 10000,
|
||||||
|
env: env
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return sshLatestTag.trim().replace('v', '');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SSH Git error:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check public repository using GitHub API
|
||||||
|
async checkPublicRepo(owner, repo) {
|
||||||
|
try {
|
||||||
|
const httpsRepoUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
|
||||||
|
|
||||||
|
const response = await fetch(httpsRepoUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/vnd.github.v3+json',
|
||||||
|
'User-Agent': 'PatchMon-Server/1.2.4'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseData = await response.json();
|
||||||
|
return releaseData.tag_name.replace('v', '');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('GitHub API error:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare version strings (semantic versioning)
|
||||||
|
compareVersions(version1, version2) {
|
||||||
|
const v1parts = version1.split('.').map(Number);
|
||||||
|
const v2parts = version2.split('.').map(Number);
|
||||||
|
|
||||||
|
const maxLength = Math.max(v1parts.length, v2parts.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < maxLength; i++) {
|
||||||
|
const v1part = v1parts[i] || 0;
|
||||||
|
const v2part = v2parts[i] || 0;
|
||||||
|
|
||||||
|
if (v1part > v2part) return 1;
|
||||||
|
if (v1part < v2part) return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get scheduler status
|
||||||
|
getStatus() {
|
||||||
|
return {
|
||||||
|
isRunning: this.isRunning,
|
||||||
|
checkInterval: this.checkInterval,
|
||||||
|
nextCheck: this.isRunning ? new Date(Date.now() + this.checkInterval) : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create singleton instance
|
||||||
|
const updateScheduler = new UpdateScheduler();
|
||||||
|
|
||||||
|
module.exports = updateScheduler;
|
||||||
@@ -17,7 +17,9 @@
|
|||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"chart.js": "^4.4.0",
|
"chart.js": "^4.4.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
|
"express": "^4.18.2",
|
||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.294.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-chartjs-2": "^5.2.0",
|
"react-chartjs-2": "^5.2.0",
|
||||||
|
|||||||
29
frontend/server.js
Normal file
29
frontend/server.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import path from 'path';
|
||||||
|
import cors from 'cors';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Enable CORS for API calls
|
||||||
|
app.use(cors({
|
||||||
|
origin: process.env.CORS_ORIGIN || '*',
|
||||||
|
credentials: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Serve static files from dist directory
|
||||||
|
app.use(express.static(path.join(__dirname, 'dist')));
|
||||||
|
|
||||||
|
// Handle SPA routing - serve index.html for all routes
|
||||||
|
app.get('*', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Frontend server running on port ${PORT}`);
|
||||||
|
console.log(`Serving from: ${path.join(__dirname, 'dist')}`);
|
||||||
|
});
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import React, { createContext, useContext, useState } from 'react'
|
import React, { createContext, useContext, useState } from 'react'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { versionAPI } from '../utils/api'
|
||||||
|
|
||||||
const UpdateNotificationContext = createContext()
|
const UpdateNotificationContext = createContext()
|
||||||
|
|
||||||
@@ -11,20 +13,29 @@ export const useUpdateNotification = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UpdateNotificationProvider = ({ children }) => {
|
export const UpdateNotificationProvider = ({ children }) => {
|
||||||
const [updateAvailable, setUpdateAvailable] = useState(false)
|
const [dismissed, setDismissed] = useState(false)
|
||||||
const [updateInfo, setUpdateInfo] = useState(null)
|
|
||||||
|
// Query for update information
|
||||||
|
const { data: updateData, isLoading, error } = useQuery({
|
||||||
|
queryKey: ['updateCheck'],
|
||||||
|
queryFn: () => versionAPI.checkUpdates().then(res => res.data),
|
||||||
|
refetchInterval: 5 * 60 * 1000, // Refetch every 5 minutes
|
||||||
|
retry: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateAvailable = updateData?.isUpdateAvailable && !dismissed
|
||||||
|
const updateInfo = updateData
|
||||||
|
|
||||||
const dismissNotification = () => {
|
const dismissNotification = () => {
|
||||||
setUpdateAvailable(false)
|
setDismissed(true)
|
||||||
setUpdateInfo(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
updateAvailable,
|
updateAvailable,
|
||||||
updateInfo,
|
updateInfo,
|
||||||
dismissNotification,
|
dismissNotification,
|
||||||
isLoading: false,
|
isLoading,
|
||||||
error: null
|
error
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Save, Server, Globe, Shield, AlertCircle, CheckCircle, Code, Plus, Trash2, Star, Download, X, Settings as SettingsIcon } from 'lucide-react';
|
import { Save, Server, Globe, Shield, AlertCircle, CheckCircle, Code, Plus, Trash2, Star, Download, X, Settings as SettingsIcon, Clock } from 'lucide-react';
|
||||||
import { settingsAPI, agentVersionAPI, versionAPI } from '../utils/api';
|
import { settingsAPI, agentVersionAPI, versionAPI } from '../utils/api';
|
||||||
import { useUpdateNotification } from '../contexts/UpdateNotificationContext';
|
import { useUpdateNotification } from '../contexts/UpdateNotificationContext';
|
||||||
import UpgradeNotificationIcon from '../components/UpgradeNotificationIcon';
|
import UpgradeNotificationIcon from '../components/UpgradeNotificationIcon';
|
||||||
@@ -195,6 +195,7 @@ const Settings = () => {
|
|||||||
currentVersion: data.currentVersion,
|
currentVersion: data.currentVersion,
|
||||||
latestVersion: data.latestVersion,
|
latestVersion: data.latestVersion,
|
||||||
isUpdateAvailable: data.isUpdateAvailable,
|
isUpdateAvailable: data.isUpdateAvailable,
|
||||||
|
lastUpdateCheck: data.lastUpdateCheck,
|
||||||
checking: false,
|
checking: false,
|
||||||
error: null
|
error: null
|
||||||
});
|
});
|
||||||
@@ -950,6 +951,22 @@ const Settings = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Last Checked Time */}
|
||||||
|
{versionInfo.lastUpdateCheck && (
|
||||||
|
<div className="bg-white dark:bg-secondary-800 rounded-lg p-4 border border-secondary-200 dark:border-secondary-600">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Clock className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||||
|
<span className="text-sm font-medium text-secondary-700 dark:text-secondary-300">Last Checked</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-secondary-600 dark:text-secondary-400">
|
||||||
|
{new Date(versionInfo.lastUpdateCheck).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-secondary-500 dark:text-secondary-400 mt-1">
|
||||||
|
Updates are checked automatically every 24 hours
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -56,7 +56,9 @@
|
|||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"chart.js": "^4.4.0",
|
"chart.js": "^4.4.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
|
"express": "^4.18.2",
|
||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.294.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-chartjs-2": "^5.2.0",
|
"react-chartjs-2": "^5.2.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user