fix bugs in contrib system, remove health endpoint
This commit is contained in:
		
							parent
							
								
									3c55742dfa
								
							
						
					
					
						commit
						0ac31484d5
					
				@ -7,8 +7,7 @@
 | 
			
		||||
    "start": "astro dev",
 | 
			
		||||
    "build": "astro build",
 | 
			
		||||
    "preview": "astro preview",
 | 
			
		||||
    "astro": "astro",
 | 
			
		||||
    "check:health": "curl -f http://localhost:4321/health || exit 1"
 | 
			
		||||
    "astro": "astro"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@astrojs/node": "^9.3.0",
 | 
			
		||||
 | 
			
		||||
@ -1,478 +0,0 @@
 | 
			
		||||
// src/pages/api/contribute/health.ts
 | 
			
		||||
import type { APIRoute } from 'astro';
 | 
			
		||||
import { getSessionFromRequest, verifySession } from '../../../utils/auth.js';
 | 
			
		||||
import { GitContributionManager } from '../../../utils/gitContributions.js';
 | 
			
		||||
import { promises as fs } from 'fs';
 | 
			
		||||
import { execSync } from 'child_process';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
 | 
			
		||||
export const prerender = false;
 | 
			
		||||
 | 
			
		||||
interface HealthCheck {
 | 
			
		||||
  component: string;
 | 
			
		||||
  status: 'healthy' | 'warning' | 'error';
 | 
			
		||||
  message: string;
 | 
			
		||||
  details?: any;
 | 
			
		||||
  lastChecked: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface SystemHealth {
 | 
			
		||||
  overall: 'healthy' | 'warning' | 'error';
 | 
			
		||||
  checks: HealthCheck[];
 | 
			
		||||
  summary: {
 | 
			
		||||
    healthy: number;
 | 
			
		||||
    warnings: number;
 | 
			
		||||
    errors: number;
 | 
			
		||||
  };
 | 
			
		||||
  timestamp: string;
 | 
			
		||||
  uptime?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class HealthMonitor {
 | 
			
		||||
  private checks: HealthCheck[] = [];
 | 
			
		||||
  
 | 
			
		||||
  async runAllChecks(): Promise<SystemHealth> {
 | 
			
		||||
    this.checks = [];
 | 
			
		||||
    
 | 
			
		||||
    // Run all health checks
 | 
			
		||||
    await Promise.allSettled([
 | 
			
		||||
      this.checkGitRepository(),
 | 
			
		||||
      this.checkGitConnectivity(),
 | 
			
		||||
      this.checkDiskSpace(),
 | 
			
		||||
      this.checkMemoryUsage(),
 | 
			
		||||
      this.checkDataFiles(),
 | 
			
		||||
      this.checkAuthSystem(),
 | 
			
		||||
      this.checkEnvironmentVariables(),
 | 
			
		||||
      this.checkFilePermissions()
 | 
			
		||||
    ]);
 | 
			
		||||
    
 | 
			
		||||
    // Calculate overall status
 | 
			
		||||
    const errors = this.checks.filter(c => c.status === 'error').length;
 | 
			
		||||
    const warnings = this.checks.filter(c => c.status === 'warning').length;
 | 
			
		||||
    const healthy = this.checks.filter(c => c.status === 'healthy').length;
 | 
			
		||||
    
 | 
			
		||||
    let overall: 'healthy' | 'warning' | 'error' = 'healthy';
 | 
			
		||||
    if (errors > 0) {
 | 
			
		||||
      overall = 'error';
 | 
			
		||||
    } else if (warnings > 0) {
 | 
			
		||||
      overall = 'warning';
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    return {
 | 
			
		||||
      overall,
 | 
			
		||||
      checks: this.checks,
 | 
			
		||||
      summary: { healthy, warnings: warnings, errors },
 | 
			
		||||
      timestamp: new Date().toISOString(),
 | 
			
		||||
      uptime: this.getUptime()
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  private addCheck(component: string, status: 'healthy' | 'warning' | 'error', message: string, details?: any) {
 | 
			
		||||
    this.checks.push({
 | 
			
		||||
      component,
 | 
			
		||||
      status,
 | 
			
		||||
      message,
 | 
			
		||||
      details,
 | 
			
		||||
      lastChecked: new Date().toISOString()
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  private async checkGitRepository(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const localRepoPath = process.env.LOCAL_REPO_PATH || '/var/git/cc24-hub';
 | 
			
		||||
      
 | 
			
		||||
      // Check if repo exists
 | 
			
		||||
      try {
 | 
			
		||||
        await fs.access(localRepoPath);
 | 
			
		||||
      } catch {
 | 
			
		||||
        this.addCheck('Git Repository', 'error', 'Local git repository not found', { path: localRepoPath });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Check if it's a git repository
 | 
			
		||||
      try {
 | 
			
		||||
        execSync('git status', { cwd: localRepoPath, stdio: 'pipe' });
 | 
			
		||||
      } catch {
 | 
			
		||||
        this.addCheck('Git Repository', 'error', 'Directory is not a git repository', { path: localRepoPath });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Check repository health
 | 
			
		||||
      try {
 | 
			
		||||
        const gitStatus = execSync('git status --porcelain', { cwd: localRepoPath, encoding: 'utf8' });
 | 
			
		||||
        const uncommittedChanges = gitStatus.trim().split('\n').filter(line => line.trim()).length;
 | 
			
		||||
        
 | 
			
		||||
        const branchInfo = execSync('git branch --show-current', { cwd: localRepoPath, encoding: 'utf8' }).trim();
 | 
			
		||||
        const lastCommit = execSync('git log -1 --format="%h %s (%ar)"', { cwd: localRepoPath, encoding: 'utf8' }).trim();
 | 
			
		||||
        
 | 
			
		||||
        if (uncommittedChanges > 0) {
 | 
			
		||||
          this.addCheck('Git Repository', 'warning', `Repository has ${uncommittedChanges} uncommitted changes`, {
 | 
			
		||||
            branch: branchInfo,
 | 
			
		||||
            lastCommit,
 | 
			
		||||
            uncommittedChanges
 | 
			
		||||
          });
 | 
			
		||||
        } else {
 | 
			
		||||
          this.addCheck('Git Repository', 'healthy', 'Repository is clean and up to date', {
 | 
			
		||||
            branch: branchInfo,
 | 
			
		||||
            lastCommit
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        this.addCheck('Git Repository', 'warning', 'Could not check repository status', { error: error.message });
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.addCheck('Git Repository', 'error', 'Failed to check git repository', { error: error.message });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  private async checkGitConnectivity(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const gitManager = new GitContributionManager();
 | 
			
		||||
      const health = await gitManager.checkHealth();
 | 
			
		||||
      
 | 
			
		||||
      if (health.healthy) {
 | 
			
		||||
        this.addCheck('Git Connectivity', 'healthy', 'Git API connectivity working');
 | 
			
		||||
      } else {
 | 
			
		||||
        this.addCheck('Git Connectivity', 'error', 'Git API connectivity issues', { issues: health.issues });
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.addCheck('Git Connectivity', 'error', 'Failed to check git connectivity', { error: error.message });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  private async checkDiskSpace(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      // Get disk usage for the current working directory
 | 
			
		||||
      const stats = await fs.statfs(process.cwd());
 | 
			
		||||
      const totalSpace = stats.bavail * stats.bsize; // Available space in bytes
 | 
			
		||||
      const totalBlocks = stats.blocks * stats.bsize; // Total space in bytes
 | 
			
		||||
      const usedSpace = totalBlocks - totalSpace;
 | 
			
		||||
      const usagePercent = Math.round((usedSpace / totalBlocks) * 100);
 | 
			
		||||
      
 | 
			
		||||
      const freeSpaceGB = Math.round(totalSpace / (1024 * 1024 * 1024) * 100) / 100;
 | 
			
		||||
      const totalSpaceGB = Math.round(totalBlocks / (1024 * 1024 * 1024) * 100) / 100;
 | 
			
		||||
      
 | 
			
		||||
      const details = {
 | 
			
		||||
        freeSpace: `${freeSpaceGB} GB`,
 | 
			
		||||
        totalSpace: `${totalSpaceGB} GB`,
 | 
			
		||||
        usagePercent: `${usagePercent}%`
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
      if (usagePercent > 90) {
 | 
			
		||||
        this.addCheck('Disk Space', 'error', `Disk usage critical: ${usagePercent}%`, details);
 | 
			
		||||
      } else if (usagePercent > 80) {
 | 
			
		||||
        this.addCheck('Disk Space', 'warning', `Disk usage high: ${usagePercent}%`, details);
 | 
			
		||||
      } else {
 | 
			
		||||
        this.addCheck('Disk Space', 'healthy', `Disk usage normal: ${usagePercent}%`, details);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.addCheck('Disk Space', 'warning', 'Could not check disk space', { error: error.message });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  private async checkMemoryUsage(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const memInfo = process.memoryUsage();
 | 
			
		||||
      const totalMemMB = Math.round(memInfo.heapTotal / 1024 / 1024 * 100) / 100;
 | 
			
		||||
      const usedMemMB = Math.round(memInfo.heapUsed / 1024 / 1024 * 100) / 100;
 | 
			
		||||
      const externalMemMB = Math.round(memInfo.external / 1024 / 1024 * 100) / 100;
 | 
			
		||||
      
 | 
			
		||||
      const details = {
 | 
			
		||||
        heapUsed: `${usedMemMB} MB`,
 | 
			
		||||
        heapTotal: `${totalMemMB} MB`,
 | 
			
		||||
        external: `${externalMemMB} MB`,
 | 
			
		||||
        rss: `${Math.round(memInfo.rss / 1024 / 1024 * 100) / 100} MB`
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
      if (usedMemMB > 500) {
 | 
			
		||||
        this.addCheck('Memory Usage', 'warning', `High memory usage: ${usedMemMB} MB`, details);
 | 
			
		||||
      } else {
 | 
			
		||||
        this.addCheck('Memory Usage', 'healthy', `Memory usage normal: ${usedMemMB} MB`, details);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.addCheck('Memory Usage', 'warning', 'Could not check memory usage', { error: error.message });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  private async checkDataFiles(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const dataFiles = [
 | 
			
		||||
        'src/data/tools.yaml',
 | 
			
		||||
        'src/content/knowledgebase/'
 | 
			
		||||
      ];
 | 
			
		||||
      
 | 
			
		||||
      const fileStatuses: Array<{
 | 
			
		||||
        path: string;
 | 
			
		||||
        type?: 'file' | 'directory';
 | 
			
		||||
        fileCount?: number;
 | 
			
		||||
        size?: string;
 | 
			
		||||
        lastModified?: string;
 | 
			
		||||
        error?: string;
 | 
			
		||||
      }> = [];
 | 
			
		||||
      
 | 
			
		||||
      for (const filePath of dataFiles) {
 | 
			
		||||
        try {
 | 
			
		||||
          const stats = await fs.stat(filePath);
 | 
			
		||||
          const isDirectory = stats.isDirectory();
 | 
			
		||||
          
 | 
			
		||||
          if (isDirectory) {
 | 
			
		||||
            // Count files in directory
 | 
			
		||||
            const files = await fs.readdir(filePath);
 | 
			
		||||
            const mdFiles = files.filter(f => f.endsWith('.md')).length;
 | 
			
		||||
            fileStatuses.push({
 | 
			
		||||
              path: filePath,
 | 
			
		||||
              type: 'directory',
 | 
			
		||||
              fileCount: mdFiles,
 | 
			
		||||
              lastModified: stats.mtime.toISOString()
 | 
			
		||||
            });
 | 
			
		||||
          } else {
 | 
			
		||||
            // Check file size and modification time
 | 
			
		||||
            const fileSizeKB = Math.round(stats.size / 1024 * 100) / 100;
 | 
			
		||||
            fileStatuses.push({
 | 
			
		||||
              path: filePath,
 | 
			
		||||
              type: 'file',
 | 
			
		||||
              size: `${fileSizeKB} KB`,
 | 
			
		||||
              lastModified: stats.mtime.toISOString()
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
        } catch (error: any) {
 | 
			
		||||
          fileStatuses.push({
 | 
			
		||||
            path: filePath,
 | 
			
		||||
            error: error?.message || 'Unknown error'
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      const errors = fileStatuses.filter(f => f.error);
 | 
			
		||||
      
 | 
			
		||||
      if (errors.length > 0) {
 | 
			
		||||
        this.addCheck('Data Files', 'error', `${errors.length} data files inaccessible`, { files: fileStatuses });
 | 
			
		||||
      } else {
 | 
			
		||||
        this.addCheck('Data Files', 'healthy', 'All data files accessible', { files: fileStatuses });
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.addCheck('Data Files', 'error', 'Failed to check data files', { error: error.message });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  private async checkAuthSystem(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
 | 
			
		||||
      
 | 
			
		||||
      if (!authRequired) {
 | 
			
		||||
        this.addCheck('Authentication', 'healthy', 'Authentication disabled', { mode: 'disabled' });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      const requiredEnvVars = [
 | 
			
		||||
        'OIDC_ENDPOINT',
 | 
			
		||||
        'OIDC_CLIENT_ID',
 | 
			
		||||
        'OIDC_CLIENT_SECRET',
 | 
			
		||||
        'PUBLIC_BASE_URL'
 | 
			
		||||
      ];
 | 
			
		||||
      
 | 
			
		||||
      const missingVars = requiredEnvVars.filter(varName => !process.env[varName]);
 | 
			
		||||
      
 | 
			
		||||
      if (missingVars.length > 0) {
 | 
			
		||||
        this.addCheck('Authentication', 'error', 'Missing OIDC configuration', { 
 | 
			
		||||
          missing: missingVars,
 | 
			
		||||
          mode: 'enabled' 
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Test OIDC endpoint connectivity
 | 
			
		||||
      try {
 | 
			
		||||
        const oidcEndpoint = process.env.OIDC_ENDPOINT;
 | 
			
		||||
        const controller = new AbortController();
 | 
			
		||||
        const timeoutId = setTimeout(() => controller.abort(), 5000);
 | 
			
		||||
        
 | 
			
		||||
        const response = await fetch(`${oidcEndpoint}/.well-known/openid_configuration`, {
 | 
			
		||||
          method: 'GET',
 | 
			
		||||
          signal: controller.signal
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        clearTimeout(timeoutId);
 | 
			
		||||
        
 | 
			
		||||
        if (response.ok) {
 | 
			
		||||
          this.addCheck('Authentication', 'healthy', 'OIDC provider accessible', { 
 | 
			
		||||
            endpoint: oidcEndpoint,
 | 
			
		||||
            mode: 'enabled' 
 | 
			
		||||
          });
 | 
			
		||||
        } else {
 | 
			
		||||
          this.addCheck('Authentication', 'warning', 'OIDC provider returned error', { 
 | 
			
		||||
            endpoint: oidcEndpoint,
 | 
			
		||||
            status: response.status,
 | 
			
		||||
            mode: 'enabled' 
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        this.addCheck('Authentication', 'error', 'Cannot reach OIDC provider', { 
 | 
			
		||||
          endpoint: process.env.OIDC_ENDPOINT,
 | 
			
		||||
          error: error.message,
 | 
			
		||||
          mode: 'enabled' 
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.addCheck('Authentication', 'error', 'Failed to check auth system', { error: error.message });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  private async checkEnvironmentVariables(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const requiredVars = [
 | 
			
		||||
        'GIT_REPO_URL',
 | 
			
		||||
        'GIT_API_ENDPOINT',
 | 
			
		||||
        'GIT_API_TOKEN',
 | 
			
		||||
        'LOCAL_REPO_PATH'
 | 
			
		||||
      ];
 | 
			
		||||
      
 | 
			
		||||
      const optionalVars = [
 | 
			
		||||
        'GIT_PROVIDER',
 | 
			
		||||
        'AUTHENTICATION_NECESSARY',
 | 
			
		||||
        'NODE_ENV'
 | 
			
		||||
      ];
 | 
			
		||||
      
 | 
			
		||||
      const missingRequired = requiredVars.filter(varName => !process.env[varName]);
 | 
			
		||||
      const missingOptional = optionalVars.filter(varName => !process.env[varName]);
 | 
			
		||||
      
 | 
			
		||||
      const details = {
 | 
			
		||||
        required: {
 | 
			
		||||
          total: requiredVars.length,
 | 
			
		||||
          missing: missingRequired.length,
 | 
			
		||||
          missingVars: missingRequired
 | 
			
		||||
        },
 | 
			
		||||
        optional: {
 | 
			
		||||
          total: optionalVars.length,
 | 
			
		||||
          missing: missingOptional.length,
 | 
			
		||||
          missingVars: missingOptional
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
      if (missingRequired.length > 0) {
 | 
			
		||||
        this.addCheck('Environment Variables', 'error', `${missingRequired.length} required environment variables missing`, details);
 | 
			
		||||
      } else if (missingOptional.length > 0) {
 | 
			
		||||
        this.addCheck('Environment Variables', 'warning', `${missingOptional.length} optional environment variables missing`, details);
 | 
			
		||||
      } else {
 | 
			
		||||
        this.addCheck('Environment Variables', 'healthy', 'All environment variables configured', details);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.addCheck('Environment Variables', 'error', 'Failed to check environment variables', { error: error.message });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  private async checkFilePermissions(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const localRepoPath = process.env.LOCAL_REPO_PATH || '/var/git/cc24-hub';
 | 
			
		||||
      
 | 
			
		||||
      try {
 | 
			
		||||
        // Test read permission
 | 
			
		||||
        await fs.access(localRepoPath, fs.constants.R_OK);
 | 
			
		||||
        
 | 
			
		||||
        // Test write permission
 | 
			
		||||
        await fs.access(localRepoPath, fs.constants.W_OK);
 | 
			
		||||
        
 | 
			
		||||
        this.addCheck('File Permissions', 'healthy', 'Repository has proper read/write permissions', { path: localRepoPath });
 | 
			
		||||
        
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        if (error.code === 'EACCES') {
 | 
			
		||||
          this.addCheck('File Permissions', 'error', 'Insufficient permissions for repository', { 
 | 
			
		||||
            path: localRepoPath,
 | 
			
		||||
            error: error.message 
 | 
			
		||||
          });
 | 
			
		||||
        } else {
 | 
			
		||||
          this.addCheck('File Permissions', 'error', 'Repository path inaccessible', { 
 | 
			
		||||
            path: localRepoPath,
 | 
			
		||||
            error: error.message 
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.addCheck('File Permissions', 'warning', 'Could not check file permissions', { error: error.message });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  private getUptime(): string {
 | 
			
		||||
    const uptimeSeconds = process.uptime();
 | 
			
		||||
    const hours = Math.floor(uptimeSeconds / 3600);
 | 
			
		||||
    const minutes = Math.floor((uptimeSeconds % 3600) / 60);
 | 
			
		||||
    const seconds = Math.floor(uptimeSeconds % 60);
 | 
			
		||||
    
 | 
			
		||||
    return `${hours}h ${minutes}m ${seconds}s`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const GET: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // Check authentication for health endpoint
 | 
			
		||||
    const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
 | 
			
		||||
    
 | 
			
		||||
    if (authRequired) {
 | 
			
		||||
      const sessionToken = getSessionFromRequest(request);
 | 
			
		||||
      if (!sessionToken) {
 | 
			
		||||
        return new Response(JSON.stringify({ error: 'Authentication required' }), {
 | 
			
		||||
          status: 401,
 | 
			
		||||
          headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const session = await verifySession(sessionToken);
 | 
			
		||||
      if (!session) {
 | 
			
		||||
        return new Response(JSON.stringify({ error: 'Invalid session' }), {
 | 
			
		||||
          status: 401,
 | 
			
		||||
          headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Run health checks
 | 
			
		||||
    const monitor = new HealthMonitor();
 | 
			
		||||
    const health = await monitor.runAllChecks();
 | 
			
		||||
    
 | 
			
		||||
    // Determine HTTP status code based on overall health
 | 
			
		||||
    let statusCode = 200;
 | 
			
		||||
    if (health.overall === 'warning') {
 | 
			
		||||
      statusCode = 200; // Still OK, but with warnings
 | 
			
		||||
    } else if (health.overall === 'error') {
 | 
			
		||||
      statusCode = 503; // Service Unavailable
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    return new Response(JSON.stringify(health), {
 | 
			
		||||
      status: statusCode,
 | 
			
		||||
      headers: { 
 | 
			
		||||
        'Content-Type': 'application/json',
 | 
			
		||||
        'Cache-Control': 'no-cache, no-store, must-revalidate'
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Health check error:', error);
 | 
			
		||||
    
 | 
			
		||||
    const errorResponse: SystemHealth = {
 | 
			
		||||
      overall: 'error',
 | 
			
		||||
      checks: [{
 | 
			
		||||
        component: 'Health Monitor',
 | 
			
		||||
        status: 'error',
 | 
			
		||||
        message: 'Health check system failure',
 | 
			
		||||
        details: { error: error.message },
 | 
			
		||||
        lastChecked: new Date().toISOString()
 | 
			
		||||
      }],
 | 
			
		||||
      summary: { healthy: 0, warnings: 0, errors: 1 },
 | 
			
		||||
      timestamp: new Date().toISOString()
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    return new Response(JSON.stringify(errorResponse), {
 | 
			
		||||
      status: 500,
 | 
			
		||||
      headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@ -3,61 +3,39 @@ import type { APIRoute } from 'astro';
 | 
			
		||||
import { getSessionFromRequest, verifySession } from '../../../utils/auth.js';
 | 
			
		||||
import { GitContributionManager } from '../../../utils/gitContributions.js';
 | 
			
		||||
import { z } from 'zod';
 | 
			
		||||
import { promises as fs } from 'fs';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
 | 
			
		||||
export const prerender = false;
 | 
			
		||||
 | 
			
		||||
// Enhanced knowledgebase schema for contributions
 | 
			
		||||
// Simplified schema for document-based contributions
 | 
			
		||||
const KnowledgebaseContributionSchema = z.object({
 | 
			
		||||
  toolName: z.string().min(1, 'Tool name is required'),
 | 
			
		||||
  title: z.string().min(5, 'Title must be at least 5 characters').max(100, 'Title too long'),
 | 
			
		||||
  description: z.string().min(20, 'Description must be at least 20 characters').max(300, 'Description too long'),
 | 
			
		||||
  content: z.string().min(50, 'Content must be at least 50 characters'),
 | 
			
		||||
  difficulty: z.enum(['novice', 'beginner', 'intermediate', 'advanced', 'expert'], {
 | 
			
		||||
    errorMap: () => ({ message: 'Invalid difficulty level' })
 | 
			
		||||
  }),
 | 
			
		||||
  toolName: z.string().min(1),
 | 
			
		||||
  title: z.string().min(1),
 | 
			
		||||
  description: z.string().min(1),
 | 
			
		||||
  content: z.string().default(''),
 | 
			
		||||
  externalLink: z.string().url().optional().or(z.literal('')),
 | 
			
		||||
  difficulty: z.enum(['novice', 'beginner', 'intermediate', 'advanced', 'expert']),
 | 
			
		||||
  categories: z.string().transform(str => {
 | 
			
		||||
    try {
 | 
			
		||||
      return JSON.parse(str);
 | 
			
		||||
    } catch {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
    try { return JSON.parse(str); } catch { return []; }
 | 
			
		||||
  }).pipe(z.array(z.string()).default([])),
 | 
			
		||||
  tags: z.string().transform(str => {
 | 
			
		||||
    try {
 | 
			
		||||
      return JSON.parse(str);
 | 
			
		||||
    } catch {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
    try { return JSON.parse(str); } catch { return []; }
 | 
			
		||||
  }).pipe(z.array(z.string()).default([])),
 | 
			
		||||
  sections: z.string().transform(str => {
 | 
			
		||||
    try {
 | 
			
		||||
      return JSON.parse(str);
 | 
			
		||||
    } catch {
 | 
			
		||||
      return {};
 | 
			
		||||
    }
 | 
			
		||||
  }).pipe(z.record(z.boolean()).default({})),
 | 
			
		||||
  uploadedFiles: z.string().transform(str => {
 | 
			
		||||
    try {
 | 
			
		||||
      return JSON.parse(str);
 | 
			
		||||
    } catch {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
    try { return JSON.parse(str); } catch { return []; }
 | 
			
		||||
  }).pipe(z.array(z.any()).default([]))
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
interface KnowledgebaseContributionData {
 | 
			
		||||
  type: 'add' | 'edit';
 | 
			
		||||
  type: 'add';
 | 
			
		||||
  article: {
 | 
			
		||||
    toolName: string;
 | 
			
		||||
    title: string;
 | 
			
		||||
    description: string;
 | 
			
		||||
    content: string;
 | 
			
		||||
    externalLink?: string;
 | 
			
		||||
    difficulty: string;
 | 
			
		||||
    categories: string[];
 | 
			
		||||
    tags: string[];
 | 
			
		||||
    sections: Record<string, boolean>;
 | 
			
		||||
    uploadedFiles: any[];
 | 
			
		||||
  };
 | 
			
		||||
  metadata: {
 | 
			
		||||
@ -66,10 +44,10 @@ interface KnowledgebaseContributionData {
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Rate limiting (same pattern as tool contributions)
 | 
			
		||||
// Rate limiting
 | 
			
		||||
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
 | 
			
		||||
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour
 | 
			
		||||
const RATE_LIMIT_MAX = 10; // Max 10 submissions per hour
 | 
			
		||||
const RATE_LIMIT_MAX = 5; // Max 5 submissions per hour per user
 | 
			
		||||
 | 
			
		||||
function checkRateLimit(userEmail: string): boolean {
 | 
			
		||||
  const now = Date.now();
 | 
			
		||||
@ -88,68 +66,17 @@ function checkRateLimit(userEmail: string): boolean {
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function validateKnowledgebaseData(article: any): Promise<{ valid: boolean; errors: string[] }> {
 | 
			
		||||
  const errors: string[] = [];
 | 
			
		||||
function validateKnowledgebaseData(data: any): { valid: boolean; errors?: string[] } {
 | 
			
		||||
  // Only check that they provided SOMETHING
 | 
			
		||||
  const hasContent = data.content?.trim().length > 0;
 | 
			
		||||
  const hasLink = data.externalLink?.trim().length > 0;
 | 
			
		||||
  const hasFiles = data.uploadedFiles?.length > 0;
 | 
			
		||||
  
 | 
			
		||||
  // Check if tool exists in the database
 | 
			
		||||
  try {
 | 
			
		||||
    const { getToolsData } = await import('../../../utils/dataService.js');
 | 
			
		||||
    const data = await getToolsData();
 | 
			
		||||
    const toolExists = data.tools.some((tool: any) => tool.name === article.toolName);
 | 
			
		||||
    
 | 
			
		||||
    if (!toolExists) {
 | 
			
		||||
      errors.push(`Tool "${article.toolName}" not found in database`);
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    errors.push('Failed to validate tool existence');
 | 
			
		||||
  if (!hasContent && !hasLink && !hasFiles) {
 | 
			
		||||
    return { valid: false, errors: ['Must provide content, link, or files'] };
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Validate content quality
 | 
			
		||||
  if (article.content.trim().split(/\s+/).length < 50) {
 | 
			
		||||
    errors.push('Article content should be at least 50 words');
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Check for required sections based on difficulty
 | 
			
		||||
  const requiredSections = {
 | 
			
		||||
    'novice': ['overview'],
 | 
			
		||||
    'beginner': ['overview'],
 | 
			
		||||
    'intermediate': ['overview', 'usage_examples'],
 | 
			
		||||
    'advanced': ['overview', 'usage_examples'],
 | 
			
		||||
    'expert': ['overview', 'usage_examples', 'advanced_topics']
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
  const required = requiredSections[article.difficulty as keyof typeof requiredSections] || [];
 | 
			
		||||
  const missingSections = required.filter(section => !article.sections[section]);
 | 
			
		||||
  
 | 
			
		||||
  if (missingSections.length > 0) {
 | 
			
		||||
    errors.push(`Missing required sections for ${article.difficulty} difficulty: ${missingSections.join(', ')}`);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Validate categories and tags
 | 
			
		||||
  if (article.categories.length === 0) {
 | 
			
		||||
    errors.push('At least one category is required');
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  const maxCategories = 5;
 | 
			
		||||
  const maxTags = 10;
 | 
			
		||||
  
 | 
			
		||||
  if (article.categories.length > maxCategories) {
 | 
			
		||||
    errors.push(`Too many categories (max ${maxCategories})`);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (article.tags.length > maxTags) {
 | 
			
		||||
    errors.push(`Too many tags (max ${maxTags})`);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Validate uploaded files
 | 
			
		||||
  if (article.uploadedFiles.length > 20) {
 | 
			
		||||
    errors.push('Too many uploaded files (max 20)');
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return {
 | 
			
		||||
    valid: errors.length === 0,
 | 
			
		||||
    errors
 | 
			
		||||
  };
 | 
			
		||||
  return { valid: true }; // That's it - maximum freedom
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function generateArticleSlug(title: string, toolName: string): string {
 | 
			
		||||
@ -168,41 +95,75 @@ function generateArticleSlug(title: string, toolName: string): string {
 | 
			
		||||
  return `${toolSlug}-${baseSlug}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function generateMarkdownFrontmatter(article: any): string {
 | 
			
		||||
function generateMarkdownContent(article: any): string {
 | 
			
		||||
  const now = new Date();
 | 
			
		||||
  
 | 
			
		||||
  // Generate frontmatter
 | 
			
		||||
  const frontmatter = {
 | 
			
		||||
    title: article.title,
 | 
			
		||||
    tool_name: article.toolName,
 | 
			
		||||
    description: article.description,
 | 
			
		||||
    last_updated: now.toISOString().split('T')[0], // YYYY-MM-DD format
 | 
			
		||||
    last_updated: now.toISOString().split('T')[0],
 | 
			
		||||
    author: 'CC24-Team',
 | 
			
		||||
    difficulty: article.difficulty,
 | 
			
		||||
    categories: article.categories,
 | 
			
		||||
    tags: article.tags,
 | 
			
		||||
    sections: article.sections,
 | 
			
		||||
    review_status: 'draft'
 | 
			
		||||
    review_status: 'pending_review'
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
  return `---\n${Object.entries(frontmatter)
 | 
			
		||||
  const frontmatterYaml = Object.entries(frontmatter)
 | 
			
		||||
    .map(([key, value]) => {
 | 
			
		||||
      if (Array.isArray(value)) {
 | 
			
		||||
        return `${key}: [${value.map(v => `"${v}"`).join(', ')}]`;
 | 
			
		||||
      } else if (typeof value === 'object') {
 | 
			
		||||
        const obj = Object.entries(value)
 | 
			
		||||
          .map(([k, v]) => `  ${k}: ${v}`)
 | 
			
		||||
          .join('\n');
 | 
			
		||||
        return `${key}:\n${obj}`;
 | 
			
		||||
      } else {
 | 
			
		||||
        return `${key}: "${value}"`;
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .join('\n')}\n---\n\n`;
 | 
			
		||||
    .join('\n');
 | 
			
		||||
 | 
			
		||||
  // Generate content sections
 | 
			
		||||
  let content = `---\n${frontmatterYaml}\n---\n\n`;
 | 
			
		||||
  content += `# ${article.title}\n\n`;
 | 
			
		||||
  content += `${article.description}\n\n`;
 | 
			
		||||
  
 | 
			
		||||
  // Add user content if provided
 | 
			
		||||
  if (article.content && article.content.trim().length > 0) {
 | 
			
		||||
    content += `## Content\n\n${article.content}\n\n`;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Add external link if provided
 | 
			
		||||
  if (article.externalLink && article.externalLink.trim().length > 0) {
 | 
			
		||||
    content += `## External Resources\n\n`;
 | 
			
		||||
    content += `- [External Documentation](${article.externalLink})\n\n`;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Add uploaded files section
 | 
			
		||||
  if (article.uploadedFiles && article.uploadedFiles.length > 0) {
 | 
			
		||||
    content += `## Uploaded Files\n\n`;
 | 
			
		||||
    article.uploadedFiles.forEach((file: any) => {
 | 
			
		||||
      const fileType = file.name.toLowerCase();
 | 
			
		||||
      let icon = '📎';
 | 
			
		||||
      if (fileType.includes('.pdf')) icon = '📄';
 | 
			
		||||
      else if (fileType.match(/\.(png|jpg|jpeg|gif|webp)$/)) icon = '🖼️';
 | 
			
		||||
      else if (fileType.match(/\.(mp4|webm|mov|avi)$/)) icon = '🎥';
 | 
			
		||||
      else if (fileType.match(/\.(doc|docx)$/)) icon = '📝';
 | 
			
		||||
      else if (fileType.match(/\.(zip|tar|gz)$/)) icon = '📦';
 | 
			
		||||
      
 | 
			
		||||
      content += `- ${icon} [${file.name}](${file.url})\n`;
 | 
			
		||||
    });
 | 
			
		||||
    content += '\n';
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  content += `---\n\n`;
 | 
			
		||||
  content += `*This article was contributed via the CC24-Hub knowledge base submission system.*\n`;
 | 
			
		||||
  
 | 
			
		||||
  return content;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Extended GitContributionManager for knowledgebase
 | 
			
		||||
class KnowledgebaseGitManager extends GitContributionManager {
 | 
			
		||||
  async submitKnowledgebaseContribution(data: KnowledgebaseContributionData): Promise<{success: boolean, message: string, prUrl?: string, branchName?: string}> {
 | 
			
		||||
    const branchName = `kb-${data.type}-${Date.now()}`;
 | 
			
		||||
    const branchName = `kb-add-${Date.now()}`;
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
      // Create branch
 | 
			
		||||
@ -210,23 +171,22 @@ class KnowledgebaseGitManager extends GitContributionManager {
 | 
			
		||||
      
 | 
			
		||||
      // Generate file content
 | 
			
		||||
      const slug = generateArticleSlug(data.article.title, data.article.toolName);
 | 
			
		||||
      const frontmatter = generateMarkdownFrontmatter(data.article);
 | 
			
		||||
      const fullContent = frontmatter + data.article.content;
 | 
			
		||||
      const markdownContent = generateMarkdownContent(data.article);
 | 
			
		||||
      
 | 
			
		||||
      // Write article file
 | 
			
		||||
      const articlePath = `src/content/knowledgebase/${slug}.md`;
 | 
			
		||||
      await this.writeFile(articlePath, fullContent);
 | 
			
		||||
      
 | 
			
		||||
      // Update tools.yaml to add knowledgebase flag
 | 
			
		||||
      await this.updateToolKnowledgebaseFlag(data.article.toolName);
 | 
			
		||||
      await this.writeFile(articlePath, markdownContent);
 | 
			
		||||
      
 | 
			
		||||
      // Commit changes
 | 
			
		||||
      const commitMessage = `${data.type === 'add' ? 'Add' : 'Update'} knowledgebase article: ${data.article.title}
 | 
			
		||||
      const commitMessage = `Add knowledgebase contribution: ${data.article.title}
 | 
			
		||||
 | 
			
		||||
Contributed by: ${data.metadata.submitter}
 | 
			
		||||
Tool: ${data.article.toolName}
 | 
			
		||||
Type: Document-based contribution
 | 
			
		||||
Difficulty: ${data.article.difficulty}
 | 
			
		||||
Categories: ${data.article.categories.join(', ')}
 | 
			
		||||
Files: ${data.article.uploadedFiles.length} uploaded
 | 
			
		||||
${data.article.externalLink ? `External Link: ${data.article.externalLink}` : ''}
 | 
			
		||||
 | 
			
		||||
${data.metadata.reason ? `Reason: ${data.metadata.reason}` : ''}`;
 | 
			
		||||
      
 | 
			
		||||
@ -238,13 +198,13 @@ ${data.metadata.reason ? `Reason: ${data.metadata.reason}` : ''}`;
 | 
			
		||||
      // Create pull request
 | 
			
		||||
      const prUrl = await this.createPullRequest(
 | 
			
		||||
        branchName,
 | 
			
		||||
        `Knowledgebase: ${data.article.title}`,
 | 
			
		||||
        `Add KB Article: ${data.article.title}`,
 | 
			
		||||
        this.generateKnowledgebasePRDescription(data)
 | 
			
		||||
      );
 | 
			
		||||
      
 | 
			
		||||
      return {
 | 
			
		||||
        success: true,
 | 
			
		||||
        message: `Knowledgebase article contribution submitted successfully`,
 | 
			
		||||
        message: 'Knowledge base article submitted successfully',
 | 
			
		||||
        prUrl,
 | 
			
		||||
        branchName
 | 
			
		||||
      };
 | 
			
		||||
@ -260,69 +220,50 @@ ${data.metadata.reason ? `Reason: ${data.metadata.reason}` : ''}`;
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  private async updateToolKnowledgebaseFlag(toolName: string): Promise<void> {
 | 
			
		||||
    const toolsYamlPath = 'src/data/tools.yaml';
 | 
			
		||||
    const { load, dump } = await import('js-yaml');
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
      const content = await this.readFile(toolsYamlPath);
 | 
			
		||||
      const data = load(content) as any;
 | 
			
		||||
      
 | 
			
		||||
      // Find and update the tool
 | 
			
		||||
      const tool = data.tools.find((t: any) => t.name === toolName);
 | 
			
		||||
      if (tool) {
 | 
			
		||||
        tool.knowledgebase = true;
 | 
			
		||||
        
 | 
			
		||||
        const updatedContent = dump(data, {
 | 
			
		||||
          lineWidth: -1,
 | 
			
		||||
          noRefs: true,
 | 
			
		||||
          quotingType: '"',
 | 
			
		||||
          forceQuotes: false
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        await this.writeFile(toolsYamlPath, updatedContent);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.warn('Failed to update tools.yaml knowledgebase flag:', error);
 | 
			
		||||
      // Don't fail the entire contribution for this
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  private generateKnowledgebasePRDescription(data: KnowledgebaseContributionData): string {
 | 
			
		||||
    return `## Knowledgebase Article: ${data.article.title}
 | 
			
		||||
 | 
			
		||||
**Tool:** ${data.article.toolName}
 | 
			
		||||
**Type:** ${data.type === 'add' ? 'New Article' : 'Article Update'}
 | 
			
		||||
  private generateKnowledgebasePRDescription(data: KnowledgebaseContributionData): string {
 | 
			
		||||
    return `## Knowledge Base Article: ${data.article.title}
 | 
			
		||||
 | 
			
		||||
**Tool:** ${data.article.toolName}  
 | 
			
		||||
**Submitted by:** ${data.metadata.submitter}  
 | 
			
		||||
**Difficulty:** ${data.article.difficulty}
 | 
			
		||||
**Submitted by:** ${data.metadata.submitter}
 | 
			
		||||
 | 
			
		||||
### Article Details
 | 
			
		||||
- **Categories:** ${data.article.categories.join(', ')}
 | 
			
		||||
- **Tags:** ${data.article.tags.join(', ')}
 | 
			
		||||
- **Sections:** ${Object.entries(data.article.sections).filter(([_, enabled]) => enabled).map(([section, _]) => section).join(', ')}
 | 
			
		||||
- **Content Length:** ~${data.article.content.split(/\s+/).length} words
 | 
			
		||||
- **Description:** ${data.article.description}
 | 
			
		||||
- **Categories:** ${data.article.categories.length > 0 ? data.article.categories.join(', ') : 'None'}
 | 
			
		||||
- **Tags:** ${data.article.tags.length > 0 ? data.article.tags.join(', ') : 'None'}
 | 
			
		||||
- **Content:** ${data.article.content && data.article.content.trim().length > 0 ? `~${data.article.content.split(/\s+/).length} words` : 'Document/link-based'}
 | 
			
		||||
- **Uploaded Files:** ${data.article.uploadedFiles.length} files
 | 
			
		||||
${data.article.externalLink ? `- **External Link:** ${data.article.externalLink}` : ''}
 | 
			
		||||
 | 
			
		||||
### Description
 | 
			
		||||
${data.article.description}
 | 
			
		||||
${data.metadata.reason ? `### Reason for Contribution
 | 
			
		||||
${data.metadata.reason}
 | 
			
		||||
 | 
			
		||||
${data.metadata.reason ? `### Reason for Contribution\n${data.metadata.reason}\n` : ''}
 | 
			
		||||
` : ''}### Content Overview
 | 
			
		||||
${data.article.content && data.article.content.trim().length > 0 ? `
 | 
			
		||||
**Provided Content:**
 | 
			
		||||
${data.article.content.substring(0, 200)}${data.article.content.length > 200 ? '...' : ''}
 | 
			
		||||
` : ''}
 | 
			
		||||
${data.article.uploadedFiles.length > 0 ? `
 | 
			
		||||
**Uploaded Files:**
 | 
			
		||||
${data.article.uploadedFiles.map((file: any) => `- ${file.name} (${file.url})`).join('\n')}
 | 
			
		||||
` : ''}
 | 
			
		||||
 | 
			
		||||
### Review Checklist
 | 
			
		||||
- [ ] Article content is accurate and helpful
 | 
			
		||||
- [ ] Language is clear and appropriate for the difficulty level
 | 
			
		||||
- [ ] All sections are properly structured
 | 
			
		||||
- [ ] All uploaded files are accessible and appropriate
 | 
			
		||||
- [ ] External links are valid and safe
 | 
			
		||||
- [ ] Categories and tags are relevant
 | 
			
		||||
- [ ] Difficulty level is appropriate
 | 
			
		||||
- [ ] Content is well-organized and clear
 | 
			
		||||
- [ ] No sensitive or inappropriate content
 | 
			
		||||
- [ ] Links and references are valid
 | 
			
		||||
- [ ] Media files (if any) are appropriate
 | 
			
		||||
- [ ] Proper attribution for external sources
 | 
			
		||||
 | 
			
		||||
### Files Changed
 | 
			
		||||
- \`src/content/knowledgebase/${generateArticleSlug(data.article.title, data.article.toolName)}.md\` (${data.type})
 | 
			
		||||
- \`src/data/tools.yaml\` (knowledgebase flag update)
 | 
			
		||||
- \`src/content/knowledgebase/${generateArticleSlug(data.article.title, data.article.toolName)}.md\` (new article)
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
*This contribution was submitted via the CC24-Hub knowledgebase editor.*`;
 | 
			
		||||
*This contribution was submitted via the CC24-Hub document-based knowledge base system.*`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -392,12 +333,12 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Additional knowledgebase-specific validation
 | 
			
		||||
      const kbValidation = await validateKnowledgebaseData(validatedData);
 | 
			
		||||
      // Additional validation
 | 
			
		||||
      const kbValidation = validateKnowledgebaseData(validatedData);
 | 
			
		||||
      if (!kbValidation.valid) {
 | 
			
		||||
        return new Response(JSON.stringify({ 
 | 
			
		||||
          success: false, 
 | 
			
		||||
          error: 'Knowledgebase validation failed',
 | 
			
		||||
          error: 'Content validation failed',
 | 
			
		||||
          details: kbValidation.errors
 | 
			
		||||
        }), {
 | 
			
		||||
          status: 400,
 | 
			
		||||
@ -407,7 +348,7 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
 | 
			
		||||
      // Prepare contribution data
 | 
			
		||||
      const contributionData: KnowledgebaseContributionData = {
 | 
			
		||||
        type: 'add', // For now, only support adding new articles
 | 
			
		||||
        type: 'add',
 | 
			
		||||
        article: validatedData,
 | 
			
		||||
        metadata: {
 | 
			
		||||
          submitter: userEmail,
 | 
			
		||||
@ -420,7 +361,6 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
      const result = await gitManager.submitKnowledgebaseContribution(contributionData);
 | 
			
		||||
 | 
			
		||||
      if (result.success) {
 | 
			
		||||
        // Log successful contribution
 | 
			
		||||
        console.log(`[KB CONTRIBUTION] "${validatedData.title}" for ${validatedData.toolName} by ${userEmail} - PR: ${result.prUrl}`);
 | 
			
		||||
        
 | 
			
		||||
        return new Response(JSON.stringify({
 | 
			
		||||
@ -433,7 +373,6 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
          headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        // Log failed contribution
 | 
			
		||||
        console.error(`[KB CONTRIBUTION FAILED] "${validatedData.title}" by ${userEmail}: ${result.message}`);
 | 
			
		||||
        
 | 
			
		||||
        return new Response(JSON.stringify({
 | 
			
		||||
 | 
			
		||||
@ -346,48 +346,4 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
      headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Health check endpoint
 | 
			
		||||
export const GET: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // Simple authentication check for health endpoint
 | 
			
		||||
    const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
 | 
			
		||||
    
 | 
			
		||||
    if (authRequired) {
 | 
			
		||||
      const sessionToken = getSessionFromRequest(request);
 | 
			
		||||
      if (!sessionToken) {
 | 
			
		||||
        return new Response(JSON.stringify({ error: 'Authentication required' }), {
 | 
			
		||||
          status: 401,
 | 
			
		||||
          headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const session = await verifySession(sessionToken);
 | 
			
		||||
      if (!session) {
 | 
			
		||||
        return new Response(JSON.stringify({ error: 'Invalid session' }), {
 | 
			
		||||
          status: 401,
 | 
			
		||||
          headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const gitManager = new GitContributionManager();
 | 
			
		||||
    const health = await gitManager.checkHealth();
 | 
			
		||||
    
 | 
			
		||||
    return new Response(JSON.stringify(health), {
 | 
			
		||||
      status: health.healthy ? 200 : 503,
 | 
			
		||||
      headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Health check error:', error);
 | 
			
		||||
    return new Response(JSON.stringify({ 
 | 
			
		||||
      healthy: false, 
 | 
			
		||||
      issues: ['Health check failed'] 
 | 
			
		||||
    }), {
 | 
			
		||||
      status: 503,
 | 
			
		||||
      headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@ -2,6 +2,7 @@
 | 
			
		||||
// src/pages/contribute/index.astro - Updated for Phase 3
 | 
			
		||||
import BaseLayout from '../../layouts/BaseLayout.astro';
 | 
			
		||||
import { getSessionFromRequest, verifySession } from '../../utils/auth.js';
 | 
			
		||||
import { getAuthContext, requireAuth } from '../../utils/serverAuth.js';
 | 
			
		||||
 | 
			
		||||
export const prerender = false;
 | 
			
		||||
 | 
			
		||||
@ -20,9 +21,9 @@ if (authRequired) {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (!isAuthenticated) {
 | 
			
		||||
    return Astro.redirect('/auth/login');
 | 
			
		||||
  }
 | 
			
		||||
  const authContext = await getAuthContext(Astro);
 | 
			
		||||
  const authRedirect = requireAuth(authContext, Astro.url.toString());
 | 
			
		||||
  if (authRedirect) return authRedirect;
 | 
			
		||||
}
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
@ -148,12 +149,6 @@ if (authRequired) {
 | 
			
		||||
              </svg>
 | 
			
		||||
              Report Issue
 | 
			
		||||
            </a>
 | 
			
		||||
            <a href="/api/contribute/health" class="btn btn-secondary" target="_blank">
 | 
			
		||||
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
 | 
			
		||||
                <path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
 | 
			
		||||
              </svg>
 | 
			
		||||
              System Health
 | 
			
		||||
            </a>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
@ -199,95 +194,7 @@ if (authRequired) {
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- System Status -->
 | 
			
		||||
    <div class="card" style="background-color: var(--color-bg-secondary);">
 | 
			
		||||
      <h3 style="margin-bottom: 1rem; color: var(--color-text);">System Status</h3>
 | 
			
		||||
      <div id="system-status" style="display: flex; align-items: center; gap: 1rem;">
 | 
			
		||||
        <div style="width: 12px; height: 12px; background-color: var(--color-text-secondary); border-radius: 50%; animation: pulse 2s infinite;"></div>
 | 
			
		||||
        <span style="color: var(--color-text-secondary);">Checking system health...</span>
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <div style="margin-top: 1rem; font-size: 0.875rem; color: var(--color-text-secondary);">
 | 
			
		||||
        <p style="margin: 0;">
 | 
			
		||||
          <strong>Features Available:</strong> Tool contributions, knowledgebase articles, media uploads
 | 
			
		||||
        </p>
 | 
			
		||||
        <p style="margin: 0.5rem 0 0 0;">
 | 
			
		||||
          <strong>Storage:</strong> Local + Nextcloud (if configured) | 
 | 
			
		||||
          <strong>Authentication:</strong> {authRequired ? 'Required' : 'Disabled'} |
 | 
			
		||||
          <strong>Rate Limiting:</strong> Active
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </section>
 | 
			
		||||
 | 
			
		||||
  <style>
 | 
			
		||||
    .card:hover {
 | 
			
		||||
      transform: translateY(-2px);
 | 
			
		||||
      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @keyframes pulse {
 | 
			
		||||
      0%, 100% { opacity: 1; }
 | 
			
		||||
      50% { opacity: 0.5; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .loading {
 | 
			
		||||
      animation: pulse 2s infinite;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @media (max-width: 768px) {
 | 
			
		||||
      div[style*="grid-template-columns: 2fr 1fr"] {
 | 
			
		||||
        grid-template-columns: 1fr !important;
 | 
			
		||||
        gap: 1rem !important;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      div[style*="grid-template-columns: repeat(auto-fit, minmax(300px, 1fr))"] {
 | 
			
		||||
        grid-template-columns: 1fr !important;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      h1 {
 | 
			
		||||
        font-size: 2rem !important;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  </style>
 | 
			
		||||
 | 
			
		||||
  <script>
 | 
			
		||||
    // Check system health on page load
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', async function() {
 | 
			
		||||
      const statusEl = document.getElementById('system-status');
 | 
			
		||||
      
 | 
			
		||||
      if (!statusEl) return;
 | 
			
		||||
      
 | 
			
		||||
      try {
 | 
			
		||||
        const response = await fetch('/api/contribute/health');
 | 
			
		||||
        const health = await response.json();
 | 
			
		||||
        
 | 
			
		||||
        let statusColor = 'var(--color-success)';
 | 
			
		||||
        let statusText = 'All systems operational';
 | 
			
		||||
        
 | 
			
		||||
        if (health.overall === 'warning') {
 | 
			
		||||
          statusColor = 'var(--color-warning)';
 | 
			
		||||
          statusText = `${health.summary.warnings} warning(s) detected`;
 | 
			
		||||
        } else if (health.overall === 'error') {
 | 
			
		||||
          statusColor = 'var(--color-error)';
 | 
			
		||||
          statusText = `${health.summary.errors} error(s) detected`;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        statusEl.innerHTML = `
 | 
			
		||||
          <div style="width: 12px; height: 12px; background-color: ${statusColor}; border-radius: 50%;"></div>
 | 
			
		||||
          <span style="color: var(--color-text);">${statusText}</span>
 | 
			
		||||
          <a href="/api/contribute/health" target="_blank" style="color: var(--color-primary); text-decoration: underline; font-size: 0.875rem;">View Details</a>
 | 
			
		||||
        `;
 | 
			
		||||
        
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error('Health check failed:', error);
 | 
			
		||||
        statusEl.innerHTML = `
 | 
			
		||||
          <div style="width: 12px; height: 12px; background-color: var(--color-error); border-radius: 50%;"></div>
 | 
			
		||||
          <span style="color: var(--color-error);">Health check failed</span>
 | 
			
		||||
        `;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Add hover effects for cards
 | 
			
		||||
    document.querySelectorAll('.card[onclick]').forEach((card) => {
 | 
			
		||||
      const cardEl = card as HTMLElement;
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -432,66 +432,4 @@ This contribution contains the raw tool data for manual review and integration.`
 | 
			
		||||
  ---
 | 
			
		||||
  *This contribution was submitted via the CC24-Hub web interface and contains only the raw tool data for manual integration.*`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async checkHealth(): Promise<{healthy: boolean, issues?: string[]}> {
 | 
			
		||||
    const issues: string[] = [];
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
      // Check if local repo exists and is accessible
 | 
			
		||||
      try {
 | 
			
		||||
        await fs.access(this.config.localRepoPath);
 | 
			
		||||
      } catch {
 | 
			
		||||
        issues.push('Local repository path not accessible');
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Check git status
 | 
			
		||||
      try {
 | 
			
		||||
        execSync('git status', { cwd: this.config.localRepoPath, stdio: 'pipe' });
 | 
			
		||||
      } catch {
 | 
			
		||||
        issues.push('Local repository is not a valid git repository');
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Test API connectivity
 | 
			
		||||
      try {
 | 
			
		||||
        let testUrl: string;
 | 
			
		||||
        switch (this.config.provider) {
 | 
			
		||||
          case 'gitea':
 | 
			
		||||
            testUrl = `${this.config.apiEndpoint}/repos/${this.config.repoOwner}/${this.config.repoName}`;
 | 
			
		||||
            break;
 | 
			
		||||
          case 'github':
 | 
			
		||||
            testUrl = `${this.config.apiEndpoint}/repos/${this.config.repoOwner}/${this.config.repoName}`;
 | 
			
		||||
            break;
 | 
			
		||||
          case 'gitlab':
 | 
			
		||||
            testUrl = `${this.config.apiEndpoint}/projects/${encodeURIComponent(this.config.repoOwner + '/' + this.config.repoName)}`;
 | 
			
		||||
            break;
 | 
			
		||||
          default:
 | 
			
		||||
            throw new Error('Unknown provider');
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        const response = await fetch(testUrl, {
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Authorization': `Bearer ${this.config.apiToken}`
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        if (!response.ok) {
 | 
			
		||||
          issues.push(`API connectivity failed: ${response.status}`);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        issues.push(`API test failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      return {
 | 
			
		||||
        healthy: issues.length === 0,
 | 
			
		||||
        issues: issues.length > 0 ? issues : undefined
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return {
 | 
			
		||||
        healthy: false,
 | 
			
		||||
        issues: [`Health check failed: ${error instanceof Error ? error.message : 'Unknown error'}`]
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -168,6 +168,10 @@ export class NextcloudUploader {
 | 
			
		||||
      const categoryPath = this.sanitizeFilename(category);
 | 
			
		||||
      const remotePath = `${this.config.uploadPath}/${categoryPath}/${uniqueFilename}`;
 | 
			
		||||
      
 | 
			
		||||
      // **FIX: Ensure directory exists before upload**
 | 
			
		||||
      const dirPath = `${this.config.uploadPath}/${categoryPath}`;
 | 
			
		||||
      await this.ensureDirectoryExists(dirPath);
 | 
			
		||||
      
 | 
			
		||||
      // Convert file to buffer
 | 
			
		||||
      const arrayBuffer = await file.arrayBuffer();
 | 
			
		||||
      const buffer = Buffer.from(arrayBuffer);
 | 
			
		||||
@ -207,6 +211,36 @@ export class NextcloudUploader {
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async ensureDirectoryExists(dirPath: string): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      // Split path and create each directory level
 | 
			
		||||
      const parts = dirPath.split('/').filter(part => part);
 | 
			
		||||
      let currentPath = '';
 | 
			
		||||
      
 | 
			
		||||
      for (const part of parts) {
 | 
			
		||||
        currentPath += '/' + part;
 | 
			
		||||
        
 | 
			
		||||
        const mkcolUrl = `${this.config.endpoint}/remote.php/dav/files/${this.config.username}${currentPath}`;
 | 
			
		||||
        
 | 
			
		||||
        const response = await fetch(mkcolUrl, {
 | 
			
		||||
          method: 'MKCOL',
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Authorization': `Basic ${Buffer.from(`${this.config.username}:${this.config.password}`).toString('base64')}`
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        // 201 = created, 405 = already exists, both are fine
 | 
			
		||||
        if (response.status !== 201 && response.status !== 405) {
 | 
			
		||||
          console.warn(`Directory creation failed: ${response.status} for ${currentPath}`);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.warn('Failed to ensure directory exists:', error);
 | 
			
		||||
      // Don't fail upload for directory creation issues
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /**
 | 
			
		||||
   * Create a public share link for the uploaded file
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user