Compare commits

..

12 Commits

Author SHA1 Message Date
Muhammad Ibrahim
5bdd0b5830 upgraded version 2025-09-18 02:09:42 +01:00
Muhammad Ibrahim
98cadb1ff1 added the right version of patchmon 2025-09-18 01:50:15 +01:00
Muhammad Ibrahim
42a6b7e19c added ability to save the custom ssh id path 2025-09-18 01:31:07 +01:00
Muhammad Ibrahim
e35f96d30f added deploy key custom ssh path 2025-09-18 01:19:43 +01:00
Muhammad Ibrahim
08f82bc795 added ability to specify deploy key path 2025-09-18 01:02:51 +01:00
Muhammad Ibrahim
c497c1db2a chore: Remove manage-patchmon.sh from Git tracking
- Remove manage-patchmon.sh from Git tracking as it's in .gitignore
- File should only exist locally and be hosted separately
- This prevents accidental commits of the management script
2025-09-17 23:44:48 +01:00
Muhammad Ibrahim
5b7e7216e8 fix: Improve Prisma migration execution with multiple fallback methods
- Add multiple fallback methods to run Prisma migrations
- Try npx first, then direct binary paths with chmod +x
- Add fallback to install Prisma if not found
- Apply same fixes to both update_instance and small management script
- Resolves 'Permission denied' error when running prisma migrate deploy

This ensures migrations work even when npx has permission issues
or when Prisma binaries need executable permissions.
2025-09-17 23:43:23 +01:00
Muhammad Ibrahim
fe5fb92e48 fix: Add git safe.directory configuration for update commands
- Add 'git config --global --add safe.directory' before git pull in update_instance
- Add same fix to small management script update command
- Resolves 'dubious ownership' error when updating repositories
  that were cloned as root but accessed by different users

This fixes the git ownership security warning that prevents
git pull from working during updates.
2025-09-17 23:41:54 +01:00
Muhammad Ibrahim
c8d54facb9 fix: Use .env file for database credentials in update commands
- Update update_instance function to read database credentials from .env file
- Fix database backup to use credentials from .env instead of hardcoded values
- Update migration commands to load environment variables from .env
- Fix small management script update command to use .env credentials

This resolves the 'password authentication failed' error when running
update commands on existing instances where database credentials
are stored in the .env file rather than hardcoded.
2025-09-17 23:39:00 +01:00
Muhammad Ibrahim
f97b300158 fix: Add database migrations to update command
- Add 'npx prisma migrate deploy' to the update command in manage.sh
- Ensures database schema is updated when running update command
- Fixes issue where new features (like version checking) wouldn't work
  after updating without running migrations

The update_instance function already had migrations, but the
manage.sh update command was missing this crucial step.
2025-09-17 22:21:35 +01:00
Muhammad Ibrahim
17ffa48158 fix: Fix SSH command escaping for version checking
- Fix sed command escaping in git ls-remote command
- Add explicit SSH key path and GIT_SSH_COMMAND environment
- Add debug logging for troubleshooting SSH issues
- Ensure proper SSH authentication for private repositories

This resolves the 'Failed to fetch repository information' error
when checking for updates from private GitHub repositories.
2025-09-17 22:19:12 +01:00
Muhammad Ibrahim
16821d6b5e feat: Implement SSH-based version checking for private repositories
- Replace HTTPS GitHub API calls with SSH git commands
- Use existing deploy key for private repository access
- Add proper error handling for SSH authentication issues
- Support fetching latest tags via git ls-remote
- Maintain compatibility with private repositories using deploy keys

This allows the version checking system to work with private repositories
that have SSH deploy keys configured, using the same authentication
as the local git operations.
2025-09-17 22:16:52 +01:00
11 changed files with 607 additions and 56 deletions

View File

@@ -6,7 +6,7 @@
# Configuration
PATCHMON_SERVER="${PATCHMON_SERVER:-http://localhost:3001}"
API_VERSION="v1"
AGENT_VERSION="1.2.3"
AGENT_VERSION="1.2.4"
CONFIG_FILE="/etc/patchmon/agent.conf"
CREDENTIALS_FILE="/etc/patchmon/credentials"
LOG_FILE="/var/log/patchmon-agent.log"

View File

@@ -1,6 +1,6 @@
{
"name": "patchmon-backend",
"version": "1.0.0",
"version": "1.2.4",
"description": "Backend API for Linux Patch Monitoring System",
"main": "src/server.js",
"scripts": {

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "settings" ADD COLUMN "ssh_key_path" TEXT;

View File

@@ -180,6 +180,7 @@ model Settings {
updateInterval Int @map("update_interval") @default(60) // Update interval in minutes
autoUpdate Boolean @map("auto_update") @default(false) // Enable automatic agent updates
githubRepoUrl String @map("github_repo_url") @default("git@github.com:9technologygroup/patchmon.net.git") // GitHub repository URL for version checking
sshKeyPath String? @map("ssh_key_path") // Optional SSH key path for deploy key authentication
createdAt DateTime @map("created_at") @default(now())
updatedAt DateTime @map("updated_at") @updatedAt

View File

@@ -123,7 +123,16 @@ router.put('/', authenticateToken, requireManageSettings, [
body('frontendUrl').isLength({ min: 1 }).withMessage('Frontend URL is required'),
body('updateInterval').isInt({ min: 5, max: 1440 }).withMessage('Update interval must be between 5 and 1440 minutes'),
body('autoUpdate').isBoolean().withMessage('Auto update must be a boolean'),
body('githubRepoUrl').optional().isLength({ min: 1 }).withMessage('GitHub repo URL must be a non-empty string')
body('githubRepoUrl').optional().isLength({ min: 1 }).withMessage('GitHub repo URL must be a non-empty string'),
body('sshKeyPath').optional().custom((value) => {
if (value && value.trim().length === 0) {
return true; // Allow empty string
}
if (value && value.trim().length < 1) {
throw new Error('SSH key path must be a non-empty string');
}
return true;
})
], async (req, res) => {
try {
console.log('Settings update request body:', req.body);
@@ -133,8 +142,8 @@ router.put('/', authenticateToken, requireManageSettings, [
return res.status(400).json({ errors: errors.array() });
}
const { serverProtocol, serverHost, serverPort, frontendUrl, updateInterval, autoUpdate, githubRepoUrl } = req.body;
console.log('Extracted values:', { serverProtocol, serverHost, serverPort, frontendUrl, updateInterval, autoUpdate, githubRepoUrl });
const { serverProtocol, serverHost, serverPort, frontendUrl, updateInterval, autoUpdate, githubRepoUrl, sshKeyPath } = req.body;
console.log('Extracted values:', { serverProtocol, serverHost, serverPort, frontendUrl, updateInterval, autoUpdate, githubRepoUrl, sshKeyPath });
// Construct server URL from components
const serverUrl = `${serverProtocol}://${serverHost}:${serverPort}`;
@@ -165,7 +174,8 @@ router.put('/', authenticateToken, requireManageSettings, [
frontendUrl,
updateInterval: updateInterval || 60,
autoUpdate: autoUpdate || false,
githubRepoUrl: githubRepoUrl || 'git@github.com:9technologygroup/patchmon.net.git'
githubRepoUrl: githubRepoUrl || 'git@github.com:9technologygroup/patchmon.net.git',
sshKeyPath: sshKeyPath || null
}
});
console.log('Settings updated successfully:', settings);
@@ -186,7 +196,8 @@ router.put('/', authenticateToken, requireManageSettings, [
frontendUrl,
updateInterval: updateInterval || 60,
autoUpdate: autoUpdate || false,
githubRepoUrl: githubRepoUrl || 'git@github.com:9technologygroup/patchmon.net.git'
githubRepoUrl: githubRepoUrl || 'git@github.com:9technologygroup/patchmon.net.git',
sshKeyPath: sshKeyPath || null
}
});
}

View File

@@ -0,0 +1,309 @@
const express = require('express');
const { authenticateToken } = require('../middleware/auth');
const { requireManageSettings } = require('../middleware/permissions');
const { PrismaClient } = require('@prisma/client');
const { exec } = require('child_process');
const { promisify } = require('util');
const prisma = new PrismaClient();
const execAsync = promisify(exec);
const router = express.Router();
// Get current version info
router.get('/current', authenticateToken, async (req, res) => {
try {
// For now, return hardcoded version - this should match your agent version
const currentVersion = '1.2.4';
res.json({
version: currentVersion,
buildDate: new Date().toISOString(),
environment: process.env.NODE_ENV || 'development'
});
} catch (error) {
console.error('Error getting current version:', error);
res.status(500).json({ error: 'Failed to get current version' });
}
});
// Test SSH key permissions and GitHub access
router.post('/test-ssh-key', authenticateToken, requireManageSettings, async (req, res) => {
try {
const { sshKeyPath, githubRepoUrl } = req.body;
if (!sshKeyPath || !githubRepoUrl) {
return res.status(400).json({
error: 'SSH key path and GitHub repo URL are required'
});
}
// Parse repository info
let owner, repo;
if (githubRepoUrl.includes('git@github.com:')) {
const match = githubRepoUrl.match(/git@github\.com:([^\/]+)\/([^\/]+)\.git/);
if (match) {
[, owner, repo] = match;
}
} else if (githubRepoUrl.includes('github.com/')) {
const match = githubRepoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/);
if (match) {
[, owner, repo] = match;
}
}
if (!owner || !repo) {
return res.status(400).json({
error: 'Invalid GitHub repository URL format'
});
}
// Check if SSH key file exists and is readable
try {
require('fs').accessSync(sshKeyPath);
} catch (e) {
return res.status(400).json({
error: 'SSH key file not found or not accessible',
details: `Cannot access: ${sshKeyPath}`,
suggestion: 'Check the file path and ensure the application has read permissions'
});
}
// Test SSH connection to GitHub
const sshRepoUrl = `git@github.com:${owner}/${repo}.git`;
const env = {
...process.env,
GIT_SSH_COMMAND: `ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -o ConnectTimeout=10`
};
try {
// Test with a simple git command
const { stdout } = await execAsync(
`git ls-remote --heads ${sshRepoUrl} | head -n 1`,
{
timeout: 15000,
env: env
}
);
if (stdout.trim()) {
return res.json({
success: true,
message: 'SSH key is working correctly',
details: {
sshKeyPath,
repository: `${owner}/${repo}`,
testResult: 'Successfully connected to GitHub'
}
});
} else {
return res.status(400).json({
error: 'SSH connection succeeded but no data returned',
suggestion: 'Check repository access permissions'
});
}
} catch (sshError) {
console.error('SSH test error:', sshError.message);
if (sshError.message.includes('Permission denied')) {
return res.status(403).json({
error: 'SSH key permission denied',
details: 'The SSH key exists but GitHub rejected the connection',
suggestion: 'Verify the SSH key is added to the repository as a deploy key with read access'
});
} else if (sshError.message.includes('Host key verification failed')) {
return res.status(403).json({
error: 'Host key verification failed',
suggestion: 'This is normal for first-time connections. The key will be added to known_hosts automatically.'
});
} else if (sshError.message.includes('Connection timed out')) {
return res.status(408).json({
error: 'Connection timed out',
suggestion: 'Check your internet connection and GitHub status'
});
} else {
return res.status(500).json({
error: 'SSH connection failed',
details: sshError.message,
suggestion: 'Check the SSH key format and repository URL'
});
}
}
} catch (error) {
console.error('SSH key test error:', error);
res.status(500).json({
error: 'Failed to test SSH key',
details: error.message
});
}
});
// Check for updates from GitHub
router.get('/check-updates', authenticateToken, requireManageSettings, async (req, res) => {
try {
// Get GitHub repo URL from settings
const settings = await prisma.settings.findFirst();
if (!settings || !settings.githubRepoUrl) {
return res.status(400).json({ error: 'GitHub repository URL not configured' });
}
// Extract owner and repo from GitHub URL
// Support both SSH and HTTPS formats:
// git@github.com:owner/repo.git
// https://github.com/owner/repo.git
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\/([^\/]+)\/([^\/]+)/);
if (match) {
[, owner, repo] = match;
}
}
if (!owner || !repo) {
return res.status(400).json({ error: 'Invalid GitHub repository URL format' });
}
// Use SSH with deploy keys (secure approach)
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: latestTag } = await execAsync(
`git ls-remote --tags --sort=-version:refname ${sshRepoUrl} | head -n 1 | sed 's/.*refs\\/tags\\///' | sed 's/\\^{}//'`,
{
timeout: 10000,
env: env
}
);
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,
latestRelease: {
tagName: latestTag.trim(),
version: latestVersion,
repository: `${owner}/${repo}`,
sshUrl: sshRepoUrl,
sshKeyUsed: sshKeyPath
}
});
} catch (sshError) {
console.error('SSH Git error:', sshError.message);
if (sshError.message.includes('Permission denied') || sshError.message.includes('Host key verification failed')) {
return res.status(403).json({
error: 'SSH access denied to repository',
suggestion: 'Ensure your deploy key is properly configured and has access to the repository. Check that the key has read access to the repository.'
});
}
if (sshError.message.includes('not found') || sshError.message.includes('does not exist')) {
return res.status(404).json({
error: 'Repository not found',
suggestion: 'Check that the repository URL is correct and accessible with the deploy key.'
});
}
if (sshError.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/'
});
}
return res.status(500).json({
error: 'Failed to fetch repository information',
details: sshError.message,
suggestion: 'Check deploy key configuration and repository access permissions.'
});
}
} catch (error) {
console.error('Error checking for updates:', error);
res.status(500).json({
error: 'Failed to check for updates',
details: error.message
});
}
});
// Simple version comparison function
function 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;
}
module.exports = router;

View File

@@ -16,6 +16,7 @@ const permissionsRoutes = require('./routes/permissionsRoutes');
const settingsRoutes = require('./routes/settingsRoutes');
const dashboardPreferencesRoutes = require('./routes/dashboardPreferencesRoutes');
const repositoryRoutes = require('./routes/repositoryRoutes');
const versionRoutes = require('./routes/versionRoutes');
// Initialize Prisma client
const prisma = new PrismaClient();
@@ -134,6 +135,7 @@ app.use(`/api/${apiVersion}/permissions`, permissionsRoutes);
app.use(`/api/${apiVersion}/settings`, settingsRoutes);
app.use(`/api/${apiVersion}/dashboard-preferences`, dashboardPreferencesRoutes);
app.use(`/api/${apiVersion}/repositories`, repositoryRoutes);
app.use(`/api/${apiVersion}/version`, versionRoutes);
// Error handling middleware
app.use((err, req, res, next) => {

View File

@@ -1,7 +1,7 @@
{
"name": "patchmon-frontend",
"private": true,
"version": "1.0.0",
"version": "1.2.4",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
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 { settingsAPI, agentVersionAPI } from '../utils/api';
import { settingsAPI, agentVersionAPI, versionAPI } from '../utils/api';
const Settings = () => {
const [formData, setFormData] = useState({
@@ -11,7 +11,9 @@ const Settings = () => {
frontendUrl: 'http://localhost:3000',
updateInterval: 60,
autoUpdate: false,
githubRepoUrl: 'git@github.com:9technologygroup/patchmon.net.git'
githubRepoUrl: 'git@github.com:9technologygroup/patchmon.net.git',
sshKeyPath: '',
useCustomSshKey: false
});
const [errors, setErrors] = useState({});
const [isDirty, setIsDirty] = useState(false);
@@ -37,6 +39,22 @@ const Settings = () => {
isDefault: false
});
// Version checking state
const [versionInfo, setVersionInfo] = useState({
currentVersion: '1.2.4',
latestVersion: null,
isUpdateAvailable: false,
checking: false,
error: null
});
const [sshTestResult, setSshTestResult] = useState({
testing: false,
success: null,
message: null,
error: null
});
const queryClient = useQueryClient();
// Fetch current settings
@@ -57,7 +75,9 @@ const Settings = () => {
frontendUrl: settings.frontendUrl || 'http://localhost:3000',
updateInterval: settings.updateInterval || 60,
autoUpdate: settings.autoUpdate || false,
githubRepoUrl: settings.githubRepoUrl || 'git@github.com:9technologygroup/patchmon.net.git'
githubRepoUrl: settings.githubRepoUrl || 'git@github.com:9technologygroup/patchmon.net.git',
sshKeyPath: settings.sshKeyPath || '',
useCustomSshKey: !!settings.sshKeyPath
};
console.log('Setting form data to:', newFormData);
setFormData(newFormData);
@@ -80,12 +100,15 @@ const Settings = () => {
queryClient.invalidateQueries(['settings']);
// Update form data with the returned data
setFormData({
serverProtocol: data.serverProtocol || 'http',
serverHost: data.serverHost || 'localhost',
serverPort: data.serverPort || 3001,
frontendUrl: data.frontendUrl || 'http://localhost:3000',
updateInterval: data.updateInterval || 60,
autoUpdate: data.autoUpdate || false
serverProtocol: data.settings?.serverProtocol || data.serverProtocol || 'http',
serverHost: data.settings?.serverHost || data.serverHost || 'localhost',
serverPort: data.settings?.serverPort || data.serverPort || 3001,
frontendUrl: data.settings?.frontendUrl || data.frontendUrl || 'http://localhost:3000',
updateInterval: data.settings?.updateInterval || data.updateInterval || 60,
autoUpdate: data.settings?.autoUpdate || data.autoUpdate || false,
githubRepoUrl: data.settings?.githubRepoUrl || data.githubRepoUrl || 'git@github.com:9technologygroup/patchmon.net.git',
sshKeyPath: data.settings?.sshKeyPath || data.sshKeyPath || '',
useCustomSshKey: !!(data.settings?.sshKeyPath || data.sshKeyPath)
});
setIsDirty(false);
setErrors({});
@@ -152,6 +175,67 @@ const Settings = () => {
}
});
// Version checking functions
const checkForUpdates = async () => {
setVersionInfo(prev => ({ ...prev, checking: true, error: null }));
try {
const response = await versionAPI.checkUpdates();
const data = response.data;
setVersionInfo({
currentVersion: data.currentVersion,
latestVersion: data.latestVersion,
isUpdateAvailable: data.isUpdateAvailable,
checking: false,
error: null
});
} catch (error) {
console.error('Version check error:', error);
setVersionInfo(prev => ({
...prev,
checking: false,
error: error.response?.data?.error || 'Failed to check for updates'
}));
}
};
const testSshKey = async () => {
if (!formData.sshKeyPath || !formData.githubRepoUrl) {
setSshTestResult({
testing: false,
success: false,
message: null,
error: 'Please enter both SSH key path and GitHub repository URL'
});
return;
}
setSshTestResult({ testing: true, success: null, message: null, error: null });
try {
const response = await versionAPI.testSshKey({
sshKeyPath: formData.sshKeyPath,
githubRepoUrl: formData.githubRepoUrl
});
setSshTestResult({
testing: false,
success: true,
message: response.data.message,
error: null
});
} catch (error) {
console.error('SSH key test error:', error);
setSshTestResult({
testing: false,
success: false,
message: null,
error: error.response?.data?.error || 'Failed to test SSH key'
});
}
};
const handleInputChange = (field, value) => {
console.log(`handleInputChange: ${field} = ${value}`);
setFormData(prev => {
@@ -167,7 +251,16 @@ const Settings = () => {
const handleSubmit = (e) => {
e.preventDefault();
updateSettingsMutation.mutate(formData);
// Only include sshKeyPath if the toggle is enabled
const dataToSubmit = { ...formData };
if (!dataToSubmit.useCustomSshKey) {
dataToSubmit.sshKeyPath = '';
}
// Remove the frontend-only field
delete dataToSubmit.useCustomSshKey;
updateSettingsMutation.mutate(dataToSubmit);
};
const validateForm = () => {
@@ -199,7 +292,16 @@ const Settings = () => {
console.log('Saving settings:', formData);
if (validateForm()) {
console.log('Validation passed, calling mutation');
updateSettingsMutation.mutate(formData);
// Prepare data for submission
const dataToSubmit = { ...formData };
if (!dataToSubmit.useCustomSshKey) {
dataToSubmit.sshKeyPath = '';
}
// Remove the frontend-only field
delete dataToSubmit.useCustomSshKey;
updateSettingsMutation.mutate(dataToSubmit);
} else {
console.log('Validation failed:', errors);
}
@@ -688,13 +790,91 @@ const Settings = () => {
</p>
</div>
<div>
<div className="flex items-center gap-3 mb-3">
<input
type="checkbox"
id="useCustomSshKey"
checked={formData.useCustomSshKey}
onChange={(e) => {
const checked = e.target.checked;
handleInputChange('useCustomSshKey', checked);
if (!checked) {
handleInputChange('sshKeyPath', '');
}
}}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<label htmlFor="useCustomSshKey" className="text-sm font-medium text-secondary-700 dark:text-secondary-200">
Set custom SSH key path
</label>
</div>
{formData.useCustomSshKey && (
<div>
<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
SSH Key Path
</label>
<input
type="text"
value={formData.sshKeyPath || ''}
onChange={(e) => handleInputChange('sshKeyPath', e.target.value)}
className="w-full border border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white font-mono text-sm"
placeholder="/root/.ssh/id_ed25519"
/>
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
Path to your SSH deploy key. If not set, will auto-detect from common locations.
</p>
<div className="mt-3">
<button
type="button"
onClick={testSshKey}
disabled={sshTestResult.testing || !formData.sshKeyPath || !formData.githubRepoUrl}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{sshTestResult.testing ? 'Testing...' : 'Test SSH Key'}
</button>
{sshTestResult.success && (
<div className="mt-2 p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-md">
<div className="flex items-center">
<CheckCircle className="h-4 w-4 text-green-600 dark:text-green-400 mr-2" />
<p className="text-sm text-green-800 dark:text-green-200">
{sshTestResult.message}
</p>
</div>
</div>
)}
{sshTestResult.error && (
<div className="mt-2 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md">
<div className="flex items-center">
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400 mr-2" />
<p className="text-sm text-red-800 dark:text-red-200">
{sshTestResult.error}
</p>
</div>
</div>
)}
</div>
</div>
)}
{!formData.useCustomSshKey && (
<p className="text-xs text-secondary-500 dark:text-secondary-400">
Using auto-detection for SSH key location
</p>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<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">
<CheckCircle className="h-4 w-4 text-green-600 dark:text-green-400" />
<span className="text-sm font-medium text-secondary-700 dark:text-secondary-300">Current Version</span>
</div>
<span className="text-lg font-mono text-secondary-900 dark:text-white">1.2.3</span>
<span className="text-lg font-mono text-secondary-900 dark:text-white">{versionInfo.currentVersion}</span>
</div>
<div className="bg-white dark:bg-secondary-800 rounded-lg p-4 border border-secondary-200 dark:border-secondary-600">
@@ -702,52 +882,91 @@ const Settings = () => {
<Download 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">Latest Version</span>
</div>
<span className="text-lg font-mono text-secondary-900 dark:text-white">Checking...</span>
<span className="text-lg font-mono text-secondary-900 dark:text-white">
{versionInfo.checking ? (
<span className="text-blue-600 dark:text-blue-400">Checking...</span>
) : versionInfo.latestVersion ? (
<span className={versionInfo.isUpdateAvailable ? 'text-orange-600 dark:text-orange-400' : 'text-green-600 dark:text-green-400'}>
{versionInfo.latestVersion}
{versionInfo.isUpdateAvailable && ' (Update Available!)'}
</span>
) : (
<span className="text-secondary-500 dark:text-secondary-400">Not checked</span>
)}
</span>
</div>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => {
// TODO: Implement version check
console.log('Checking for updates...');
}}
className="btn-primary flex items-center gap-2"
>
<Download className="h-4 w-4" />
Check for Updates
</button>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<button
onClick={checkForUpdates}
disabled={versionInfo.checking}
className="btn-primary flex items-center gap-2"
>
<Download className="h-4 w-4" />
{versionInfo.checking ? 'Checking...' : 'Check for Updates'}
</button>
</div>
{/* Save Button for Version Settings */}
<button
onClick={() => {
// TODO: Implement update notification
console.log('Enable update notifications');
}}
className="btn-outline flex items-center gap-2"
type="button"
onClick={handleSave}
disabled={!isDirty || updateSettingsMutation.isPending}
className={`inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white ${
!isDirty || updateSettingsMutation.isPending
? 'bg-secondary-400 cursor-not-allowed'
: 'bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500'
}`}
>
<AlertCircle className="h-4 w-4" />
Enable Notifications
{updateSettingsMutation.isPending ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Saving...
</>
) : (
<>
<Save className="h-4 w-4 mr-2" />
Save Settings
</>
)}
</button>
</div>
{versionInfo.error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700 rounded-lg p-4">
<div className="flex">
<AlertCircle className="h-5 w-5 text-red-400 dark:text-red-300" />
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">Version Check Failed</h3>
<p className="mt-1 text-sm text-red-700 dark:text-red-300">
{versionInfo.error}
</p>
{versionInfo.error.includes('private') && (
<p className="mt-2 text-xs text-red-600 dark:text-red-400">
For private repositories, you may need to configure GitHub authentication or make the repository public.
</p>
)}
</div>
</div>
</div>
)}
{/* Success Message for Version Settings */}
{updateSettingsMutation.isSuccess && (
<div className="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-md p-4">
<div className="flex">
<CheckCircle className="h-5 w-5 text-green-400 dark:text-green-300" />
<div className="ml-3">
<p className="text-sm text-green-700 dark:text-green-300">Settings saved successfully!</p>
</div>
</div>
</div>
)}
</div>
</div>
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg p-4">
<div className="flex">
<AlertCircle className="h-5 w-5 text-amber-400 dark:text-amber-300" />
<div className="ml-3">
<h3 className="text-sm font-medium text-amber-800 dark:text-amber-200">Setup Instructions</h3>
<div className="mt-2 text-sm text-amber-700 dark:text-amber-300">
<p className="mb-2">To enable version checking, you need to:</p>
<ol className="list-decimal list-inside space-y-1 ml-4">
<li>Create a version tag (e.g., v1.2.3) in your GitHub repository</li>
<li>Ensure the repository is publicly accessible or configure access tokens</li>
<li>Click "Check for Updates" to verify the connection</li>
</ol>
</div>
</div>
</div>
</div>
</div>
)}
</div>

View File

@@ -178,6 +178,13 @@ export const formatDate = (date) => {
return new Date(date).toLocaleString()
}
// Version API
export const versionAPI = {
getCurrent: () => api.get('/version/current'),
checkUpdates: () => api.get('/version/check-updates'),
testSshKey: (data) => api.post('/version/test-ssh-key', data),
}
export const formatRelativeTime = (date) => {
const now = new Date()
const diff = now - new Date(date)

View File

@@ -1,6 +1,6 @@
{
"name": "patchmon",
"version": "1.0.0",
"version": "1.2.4",
"description": "Linux Patch Monitoring System",
"private": true,
"workspaces": [