← back to Designer Wallcoverings

DW-Agents/dw-agents/agent-skills-manager/skills-manager-agent.ts.backup-oauth-20251112

1255 lines

import cookieParser from "cookie-parser";
/**
 * DW-Agents: Skills Manager
 *
 * Central dashboard for managing and monitoring Claude Code skills:
 * - Lists all available skills from /root/.claude/skills/
 * - Shows skill metadata (name, description, capabilities)
 * - Displays activation status (On/Off based on cron jobs)
 * - Shows last activation time from logs
 * - Sortable by name, status, or last activation date
 * - Real-time monitoring of skill usage
 *
 * Port: 9894
 */

import express, { Request, Response } from 'express';
import session from 'express-session';
import * as fs from 'fs';
import * as path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';

import { chatMiddleware } from '../shared-chat-integration';

const execAsync = promisify(exec);

const app = express();

// No authentication required for Skills Manager
const PORT = 9894;

app.use(cookieParser());

// Add inter-agent chat widget
app.use(chatMiddleware({
  agentId: 'agent-skills-manager',
  agentName: 'Skills Manager',
  port: 9894,
  category: 'agent'
}));


// Authentication
const AUTH_USERNAME = 'admin2025';
const AUTH_PASSWORD = 'Otis';

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Session configuration
declare module 'express-session' {
  interface SessionData {
    authenticated: boolean;
  }
}

app.use(
  session({
    secret: 'dw-agents-skills-manager-2025',
    resave: true,
    saveUninitialized: false,
    rolling: true,
    cookie: {
      secure: false,
      httpOnly: true,
      maxAge: 4 * 60 * 60 * 1000, // 4 hours
      sameSite: 'lax'
    },
  })
);

// Authentication middleware
const requireAuth = (req: Request, res: Response, next: any) => {
  if (req.session.authenticated) {
    return next();
  }
  res.redirect('/login');
};

// Skill interface
interface Skill {
  id: string;
  name: string;
  description: string;
  version?: string;
  capabilities?: string[];
  triggers?: string[];
  status: 'active' | 'inactive' | 'scheduled';
  lastActivated?: Date | null;
  cronSchedules?: Array<{
    name: string;
    schedule: string;
    description: string;
  }>;
  location: string;
  hasDocumentation: boolean;
}

// Scan skills directory
// Load previous records
function loadRecords(): any {
  const recordsPath = path.join(__dirname, 'skills-records.json');
  try {
    if (fs.existsSync(recordsPath)) {
      return JSON.parse(fs.readFileSync(recordsPath, 'utf8'));
    }
  } catch (error) {
    console.error('Error loading records:', error);
  }
  return { lastUpdated: new Date().toISOString(), skills: [] };
}

// Save records
function saveRecords(skills: Skill[]) {
  const recordsPath = path.join(__dirname, 'skills-records.json');
  try {
    const records = {
      lastUpdated: new Date().toISOString(),
      skills: skills.map(s => ({
        id: s.id,
        name: s.name,
        status: s.status,
        lastActivated: s.lastActivated,
        lastSeen: new Date().toISOString()
      }))
    };
    fs.writeFileSync(recordsPath, JSON.stringify(records, null, 2));
  } catch (error) {
    console.error('Error saving records:', error);
  }
}

async function scanSkills(): Promise<Skill[]> {
  const skillsDir = '/root/.claude/skills';
  const skills: Skill[] = [];
  const previousRecords = loadRecords();

  try {
    const directories = fs.readdirSync(skillsDir, { withFileTypes: true })
      .filter(dirent => dirent.isDirectory())
      .map(dirent => dirent.name);

    for (const dir of directories) {
      const skillPath = path.join(skillsDir, dir);
      const skill: Skill = {
        id: dir,
        name: dir,
        description: 'No description available',
        status: 'inactive',
        location: skillPath,
        hasDocumentation: false,
        lastActivated: null
      };

      // Try to read skill.json
      const skillJsonPath = path.join(skillPath, 'skill.json');
      if (fs.existsSync(skillJsonPath)) {
        try {
          const skillJson = JSON.parse(fs.readFileSync(skillJsonPath, 'utf8'));
          skill.name = skillJson.name || dir;
          skill.description = skillJson.description || skill.description;
          skill.version = skillJson.version;
          skill.capabilities = skillJson.capabilities;
          skill.triggers = skillJson.triggers;
          skill.cronSchedules = skillJson.cron_schedules;
        } catch (error) {
          console.error(`Error reading skill.json for ${dir}:`, error);
        }
      } else {
        // Try to read SKILL.md for description
        const skillMdPath = path.join(skillPath, 'SKILL.md');
        if (fs.existsSync(skillMdPath)) {
          try {
            const skillMd = fs.readFileSync(skillMdPath, 'utf8');
            // Parse YAML frontmatter
            const yamlMatch = skillMd.match(/^---\n([\s\S]*?)\n---/);
            if (yamlMatch) {
              const yaml = yamlMatch[1];
              const nameMatch = yaml.match(/name:\s*(.+)/);
              const descMatch = yaml.match(/description:\s*(.+(?:\n(?![\w-]+:).+)*)/);
              if (nameMatch) {
                skill.name = nameMatch[1].trim();
              }
              if (descMatch) {
                skill.description = descMatch[1].trim().replace(/\n\s*/g, ' ');
              }
            }
            skill.hasDocumentation = true;
          } catch (error) {
            console.error(`Error reading SKILL.md for ${dir}:`, error);
          }
        }
      }

      // Check if skill has cron jobs (scheduled = active)
      if (skill.cronSchedules && skill.cronSchedules.length > 0) {
        skill.status = 'scheduled';
        // Skip syslog check - causes timeouts on large log files
      }

      // Check documentation files
      const docFiles = ['SKILL.md', 'README.md', 'skill.json'];
      skill.hasDocumentation = docFiles.some(file =>
        fs.existsSync(path.join(skillPath, file))
      );

      // Restore previous data if available
      const previousRecord = previousRecords.skills.find((r: any) => r.id === dir);
      if (previousRecord) {
        if (previousRecord.lastActivated && !skill.lastActivated) {
          skill.lastActivated = new Date(previousRecord.lastActivated);
        }
        if (previousRecord.status && skill.status === 'inactive') {
          skill.status = previousRecord.status;
        }
      }

      skills.push(skill);
    }

    // Save records for persistence
    saveRecords(skills);
  } catch (error) {
    console.error('Error scanning skills:', error);
  }

  return skills;
}

// Login page
app.get('/login', (req: Request, res: Response) => {
  if (req.session.authenticated) {
    return res.redirect('/');
  }

  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Skills Manager - Login</title>
      <style>
        * {
          margin: 0;
          padding: 0;
          box-sizing: border-box;
        }

        body {
          font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
          background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
          min-height: 100vh;
          display: flex;
          align-items: center;
          justify-content: center;
        }

        .login-container {
          background: white;
          padding: 3rem;
          border-radius: 20px;
          box-shadow: 0 20px 60px rgba(0,0,0,0.3);
          width: 100%;
          max-width: 400px;
        }

        .logo {
          text-align: center;
          margin-bottom: 2rem;
        }

        .logo-icon {
          font-size: 4rem;
          margin-bottom: 0.5rem;
        }

        h1 {
          color: #2d3748;
          font-size: 1.75rem;
          text-align: center;
          margin-bottom: 0.5rem;
        }

        .subtitle {
          color: #718096;
          text-align: center;
          margin-bottom: 2rem;
          font-size: 0.9rem;
        }

        .form-group {
          margin-bottom: 1.5rem;
        }

        label {
          display: block;
          color: #4a5568;
          font-weight: 600;
          margin-bottom: 0.5rem;
          font-size: 0.9rem;
        }

        input[type="text"],
        input[type="password"] {
          width: 100%;
          padding: 0.75rem 1rem;
          border: 2px solid #e2e8f0;
          border-radius: 10px;
          font-size: 1rem;
          transition: all 0.3s;
        }

        input[type="text"]:focus,
        input[type="password"]:focus {
          outline: none;
          border-color: #667eea;
          box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
        }

        button {
          width: 100%;
          padding: 0.875rem;
          background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
          color: white;
          border: none;
          border-radius: 10px;
          font-size: 1rem;
          font-weight: 600;
          cursor: pointer;
          transition: transform 0.2s;
        }

        button:hover {
          transform: translateY(-2px);
          box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
        }

        button:active {
          transform: translateY(0);
        }

        .error {
          background: #fed7d7;
          color: #c53030;
          padding: 0.75rem;
          border-radius: 8px;
          margin-bottom: 1rem;
          font-size: 0.9rem;
          text-align: center;
        }
      </style>
    </head>
    <body>
      <div class="login-container">
        <div class="logo">
          <div class="logo-icon">🎯</div>
          <h1>Skills Manager</h1>
          <p class="subtitle">Claude Code Skills Dashboard</p>
        </div>
        ${req.query.error ? '<div class="error">Invalid username or password</div>' : ''}
        <form method="POST" action="/login">
          <div class="form-group">
            <label>Username</label>
            <input type="text" name="username" required autofocus autocomplete="username">
          </div>
          <div class="form-group">
            <label>Password</label>
            <input type="password" name="password" required autocomplete="current-password">
          </div>
          <button type="submit">Sign In</button>
        </form>
      </div>
    </body>
    </html>
  `);
});

// Login handler
app.post('/login', (req: Request, res: Response) => {
  const { username, password } = req.body;

  if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
    req.session.authenticated = true;
    res.redirect('/');
  } else {
    res.redirect('/login?error=1');
  }
});

// Logout handler
app.post('/logout', (req: Request, res: Response) => {
  req.session.destroy((err) => {
    res.redirect('/login');
  });
});

// Main dashboard
app.get('/', async (req: Request, res: Response) => {
  // Disable caching
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, private');
  res.setHeader('Pragma', 'no-cache');
  res.setHeader('Expires', '0');

  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Skills Manager - DW-Agents</title>
      <style>
        * {
          margin: 0;
          padding: 0;
          box-sizing: border-box;
        }

        body {
          font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
          background: #f5f5f5;
          color: #36454f;
        }

        .header {
          background: linear-gradient(135deg, #36454f 0%, #708090 100%);
          color: white;
          padding: 2rem;
          box-shadow: 0 2px 4px rgba(54, 69, 79, 0.15);
        }

        .header-content {
          max-width: 1400px;
          margin: 0 auto;
          display: flex;
          justify-content: space-between;
          align-items: center;
        }

        .header-left {
          display: flex;
          align-items: center;
          gap: 1rem;
        }

        .logo {
          font-size: 2.5rem;
        }

        .header-title h1 {
          font-size: 1.75rem;
          margin-bottom: 0.25rem;
        }

        .header-subtitle {
          opacity: 0.9;
          font-size: 0.9rem;
        }

        .logout-btn {
          background: rgba(255,255,255,0.2);
          color: white;
          border: 1px solid rgba(255,255,255,0.3);
          padding: 0.5rem 1.5rem;
          border-radius: 8px;
          cursor: pointer;
          font-size: 0.9rem;
          transition: all 0.3s;
        }

        .logout-btn:hover {
          background: rgba(255,255,255,0.3);
        }

        .container {
          max-width: 1400px;
          margin: 0 auto;
          padding: 2rem;
        }

        .stats-grid {
          display: grid;
          grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
          gap: 1.5rem;
          margin-bottom: 2rem;
        }

        .stat-card {
          background: white;
          padding: 1.5rem;
          border-radius: 12px;
          box-shadow: 0 2px 8px rgba(0,0,0,0.1);
          display: flex;
          align-items: center;
          gap: 1rem;
        }

        .stat-icon {
          font-size: 2.5rem;
          width: 60px;
          height: 60px;
          display: flex;
          align-items: center;
          justify-content: center;
          border-radius: 12px;
        }

        .stat-icon.purple { background: #e8e8e8; }
        .stat-icon.green { background: #d3d3d3; }
        .stat-icon.blue { background: #e8e8e8; }
        .stat-icon.orange { background: #d3d3d3; }

        .stat-content h3 {
          font-size: 2rem;
          color: #36454f;
          margin-bottom: 0.25rem;
        }

        .stat-content p {
          color: #708090;
          font-size: 0.9rem;
        }

        .controls {
          background: white;
          padding: 1.5rem;
          border-radius: 12px;
          box-shadow: 0 2px 8px rgba(0,0,0,0.1);
          margin-bottom: 2rem;
          display: flex;
          gap: 1rem;
          align-items: center;
          flex-wrap: wrap;
        }

        .search-box {
          flex: 1;
          min-width: 300px;
        }

        .search-box input {
          width: 100%;
          padding: 0.75rem 1rem;
          border: 2px solid #e2e8f0;
          border-radius: 8px;
          font-size: 1rem;
        }

        .search-box input:focus {
          outline: none;
          border-color: #667eea;
        }

        .filter-buttons {
          display: flex;
          gap: 0.5rem;
        }

        .filter-btn {
          padding: 0.5rem 1rem;
          border: 2px solid #e2e8f0;
          background: white;
          border-radius: 8px;
          cursor: pointer;
          font-size: 0.9rem;
          transition: all 0.3s;
        }

        .filter-btn.active {
          background: #667eea;
          color: white;
          border-color: #667eea;
        }

        .filter-btn:hover {
          border-color: #667eea;
        }

        .refresh-btn {
          padding: 0.5rem 1.5rem;
          background: #48bb78;
          color: white;
          border: none;
          border-radius: 8px;
          cursor: pointer;
          font-size: 0.9rem;
          transition: all 0.3s;
        }

        .refresh-btn:hover {
          background: #38a169;
        }

        .skills-table {
          background: white;
          border-radius: 12px;
          box-shadow: 0 2px 8px rgba(0,0,0,0.1);
          overflow: hidden;
        }

        table {
          width: 100%;
          border-collapse: collapse;
        }

        thead {
          background: #f7fafc;
        }

        th {
          padding: 1rem;
          text-align: left;
          font-weight: 600;
          color: #4a5568;
          border-bottom: 2px solid #e2e8f0;
          cursor: pointer;
          user-select: none;
        }

        th:hover {
          background: #edf2f7;
        }

        th .sort-indicator {
          margin-left: 0.5rem;
          opacity: 0.5;
        }

        td {
          padding: 1rem;
          border-bottom: 1px solid #f0f0f0;
        }

        tr:hover {
          background: #f7fafc;
        }

        .skill-name {
          font-weight: 600;
          color: #2d3748;
          display: flex;
          align-items: center;
          gap: 0.5rem;
        }

        .skill-icon {
          font-size: 1.5rem;
        }

        .skill-description {
          color: #718096;
          font-size: 0.9rem;
          max-width: 500px;
          line-height: 1.4;
        }

        .status-badge {
          padding: 0.25rem 0.75rem;
          border-radius: 20px;
          font-size: 0.85rem;
          font-weight: 600;
          display: inline-block;
        }

        .status-badge.active {
          background: #d1fae5;
          color: #047857;
        }

        .status-badge.scheduled {
          background: #dbeafe;
          color: #1e40af;
        }

        .status-badge.inactive {
          background: #f3f4f6;
          color: #6b7280;
        }

        .last-activated {
          color: #718096;
          font-size: 0.9rem;
        }

        .capabilities-list {
          display: flex;
          flex-wrap: wrap;
          gap: 0.5rem;
          margin-top: 0.5rem;
        }

        .capability-tag {
          background: #f3f0ff;
          color: #667eea;
          padding: 0.25rem 0.5rem;
          border-radius: 4px;
          font-size: 0.75rem;
        }

        .cron-badge {
          background: #dbeafe;
          color: #1e40af;
          padding: 0.25rem 0.5rem;
          border-radius: 4px;
          font-size: 0.75rem;
          font-family: monospace;
        }

        .start-btn {
          background: #10b981;
          color: white;
        }

        .start-btn:hover {
          background: #059669;
          transform: translateY(-1px);
          box-shadow: 0 4px 6px rgba(0,0,0,0.1);
        }

        .stop-btn {
          background: #ef4444;
          color: white;
        }

        .stop-btn:hover {
          background: #dc2626;
          transform: translateY(-1px);
          box-shadow: 0 4px 6px rgba(0,0,0,0.1);
        }

        .action-btn:disabled {
          opacity: 0.5;
          cursor: not-allowed;
        }

        .loading {
          text-align: center;
          padding: 3rem;
          color: #718096;
        }

        .spinner {
          border: 3px solid #f3f4f6;
          border-top: 3px solid #667eea;
          border-radius: 50%;
          width: 40px;
          height: 40px;
          animation: spin 1s linear infinite;
          margin: 0 auto 1rem;
        }

        @keyframes spin {
          0% { transform: rotate(0deg); }
          100% { transform: rotate(360deg); }
        }

        .empty-state {
          text-align: center;
          padding: 3rem;
          color: #718096;
        }

        .empty-state-icon {
          font-size: 4rem;
          margin-bottom: 1rem;
        }

        @media (max-width: 768px) {
          .header-content {
            flex-direction: column;
            gap: 1rem;
          }

          .controls {
            flex-direction: column;
          }

          .search-box {
            width: 100%;
          }

          table {
            font-size: 0.9rem;
          }

          td, th {
            padding: 0.75rem 0.5rem;
          }
        }
      </style>
    </head>
    <body>
      <div class="header">
        <div class="header-content">
          <div class="header-left">
            <div class="logo">🎯</div>
            <div class="header-title">
              <h1>Skills Manager</h1>
              <p class="header-subtitle">Claude Code Skills Dashboard • Manual refresh only (auto-refresh disabled)</p>
            </div>
          </div>
          <form method="POST" action="/logout">
            <button type="submit" class="logout-btn">Logout</button>
          </form>
        </div>
      </div>

      <div class="container">
        <div class="stats-grid" id="stats">
          <div class="stat-card">
            <div class="stat-icon purple">📦</div>
            <div class="stat-content">
              <h3 id="total-skills">-</h3>
              <p>Total Skills</p>
            </div>
          </div>
          <div class="stat-card">
            <div class="stat-icon green">✅</div>
            <div class="stat-content">
              <h3 id="scheduled-skills">-</h3>
              <p>Scheduled</p>
            </div>
          </div>
          <div class="stat-card">
            <div class="stat-icon blue">📋</div>
            <div class="stat-content">
              <h3 id="documented-skills">-</h3>
              <p>Documented</p>
            </div>
          </div>
          <div class="stat-card">
            <div class="stat-icon orange">🕐</div>
            <div class="stat-content">
              <h3 id="inactive-skills">-</h3>
              <p>Inactive</p>
            </div>
          </div>
        </div>

        <div class="resources-section" style="margin: 30px 0; padding: 25px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.2);">
          <h2 style="color: white; margin: 0 0 20px 0; font-size: 1.8em; display: flex; align-items: center; gap: 10px;">
            <span>🎨</span> Useful Resources
          </h2>
          <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px;">
            <a href="https://21st.dev/community/components" target="_blank" style="display: block; padding: 20px; background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border-radius: 12px; text-decoration: none; color: white; transition: all 0.3s ease; border: 1px solid rgba(255,255,255,0.2);" onmouseover="this.style.background='rgba(255,255,255,0.25)'; this.style.transform='translateY(-5px)'; this.style.boxShadow='0 15px 40px rgba(0,0,0,0.3)';" onmouseout="this.style.background='rgba(255,255,255,0.15)'; this.style.transform='translateY(0)'; this.style.boxShadow='none';">
              <div style="font-size: 2em; margin-bottom: 10px;">🧩</div>
              <h3 style="margin: 0 0 10px 0; font-size: 1.3em;">21st.dev Components</h3>
              <p style="margin: 0; opacity: 0.9; font-size: 0.95em; line-height: 1.5;">Modern UI component library with React, Tailwind, and shadcn/ui examples. Perfect for building beautiful interfaces.</p>
            </a>
          </div>
        </div>

        <div class="controls">
          <div class="search-box">
            <input type="text" id="search" placeholder="🔍 Search skills by name or description...">
          </div>
          <div class="filter-buttons">
            <button class="filter-btn active" data-filter="all">All</button>
            <button class="filter-btn" data-filter="scheduled">Scheduled</button>
            <button class="filter-btn" data-filter="inactive">Inactive</button>
          </div>
          <button class="refresh-btn" onclick="loadSkills()">🔄 Refresh</button>
        </div>

        <div class="skills-table">
          <table>
            <thead>
              <tr>
                <th onclick="sortTable('name')">Skill Name <span class="sort-indicator">↕</span></th>
                <th onclick="sortTable('description')">Description</th>
                <th onclick="sortTable('status')">Status <span class="sort-indicator">↕</span></th>
                <th onclick="sortTable('lastActivated')">Last Activated <span class="sort-indicator">↕</span></th>
                <th>Actions</th>
              </tr>
            </thead>
            <tbody id="skills-body">
              <tr>
                <td colspan="5">
                  <div class="loading">
                    <div class="spinner"></div>
                    <p>Loading skills...</p>
                  </div>
                </td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>

      <script>
        let allSkills = [];
        let currentFilter = 'all';
        let sortColumn = 'name';
        let sortDirection = 'asc';

        // Load skills data
        async function loadSkills() {
          try {
            const response = await fetch('/api/skills');
            allSkills = await response.json();
            updateStats();
            renderSkills();
          } catch (error) {
            console.error('Error loading skills:', error);
            document.getElementById('skills-body').innerHTML = \`
              <tr>
                <td colspan="4">
                  <div class="empty-state">
                    <div class="empty-state-icon">⚠️</div>
                    <p>Error loading skills. Please try again.</p>
                  </div>
                </td>
              </tr>
            \`;
          }
        }

        // Update statistics
        function updateStats() {
          const total = allSkills.length;
          const scheduled = allSkills.filter(s => s.status === 'scheduled').length;
          const documented = allSkills.filter(s => s.hasDocumentation).length;
          const inactive = allSkills.filter(s => s.status === 'inactive').length;

          document.getElementById('total-skills').textContent = total;
          document.getElementById('scheduled-skills').textContent = scheduled;
          document.getElementById('documented-skills').textContent = documented;
          document.getElementById('inactive-skills').textContent = inactive;
        }

        // Render skills table
        function renderSkills() {
          const searchTerm = document.getElementById('search').value.toLowerCase();

          let filtered = allSkills.filter(skill => {
            const matchesSearch = skill.name.toLowerCase().includes(searchTerm) ||
                                  skill.description.toLowerCase().includes(searchTerm);
            const matchesFilter = currentFilter === 'all' || skill.status === currentFilter;
            return matchesSearch && matchesFilter;
          });

          // Sort: Recently used first, then alphabetically
          filtered.sort((a, b) => {
            // If sorting by a specific column, use that
            if (sortColumn !== 'name' && sortColumn !== 'lastActivated') {
              let aVal = a[sortColumn];
              let bVal = b[sortColumn];
              if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
              if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
              return 0;
            }

            // Default sort: Recently used first, then alphabetically
            const aTime = a.lastActivated ? new Date(a.lastActivated).getTime() : 0;
            const bTime = b.lastActivated ? new Date(b.lastActivated).getTime() : 0;

            // If one has lastActivated and the other doesn't, prioritize the one with it
            if (aTime > 0 && bTime === 0) return -1;
            if (aTime === 0 && bTime > 0) return 1;

            // If both have lastActivated, sort by most recent first
            if (aTime > 0 && bTime > 0) {
              return bTime - aTime; // Descending (most recent first)
            }

            // If neither has lastActivated (or equal times), sort alphabetically
            return a.name.localeCompare(b.name);
          });

          const tbody = document.getElementById('skills-body');

          if (filtered.length === 0) {
            tbody.innerHTML = \`
              <tr>
                <td colspan="5">
                  <div class="empty-state">
                    <div class="empty-state-icon">🔍</div>
                    <p>No skills found matching your criteria.</p>
                  </div>
                </td>
              </tr>
            \`;
            return;
          }

          tbody.innerHTML = filtered.map(skill => {
            let lastActivated = '<span style="color: #cbd5e0;">Never</span>';
            let usageDetails = '';

            if (skill.lastActivated) {
              const timeAgo = formatDate(new Date(skill.lastActivated));
              lastActivated = \`<a href="#" onclick="showUsageDetails('\${skill.id}'); return false;" style="color: #36454f; text-decoration: underline;">\${timeAgo}</a>\`;

              if (skill.lastUsage) {
                usageDetails = \`
                  <div id="usage-\${skill.id}" style="display: none; margin-top: 0.5rem; padding: 0.75rem; background: #f5f5f5; border-radius: 6px; font-size: 0.85rem;">
                    <div style="margin-bottom: 0.5rem;"><strong>📅 Time:</strong> \${skill.lastUsage.readableTime || new Date(skill.lastActivated).toLocaleString()}</div>
                    <div style="margin-bottom: 0.5rem;"><strong>📝 Usage:</strong> \${skill.lastUsage.description || 'Skill executed'}</div>
                    <div><strong>📍 Where:</strong> \${skill.lastUsage.location || 'Manual execution'}</div>
                  </div>
                \`;
              }
            }

            const capabilities = skill.capabilities
              ? \`<div class="capabilities-list">\${skill.capabilities.slice(0, 3).map(cap =>
                  \`<span class="capability-tag">\${cap}</span>\`
                ).join('')}\${skill.capabilities.length > 3 ? \`<span class="capability-tag">+\${skill.capabilities.length - 3} more</span>\` : ''}</div>\`
              : '';

            const cronBadges = skill.cronSchedules
              ? \`<div class="capabilities-list" style="margin-top: 0.5rem;">\${skill.cronSchedules.map(cron =>
                  \`<span class="cron-badge" title="\${cron.description}">\${cron.schedule}</span>\`
                ).join('')}</div>\`
              : '';

            const isRunning = skill.status === 'scheduled' || skill.status === 'active';
            const actionButtons = \`
              <div style="display: flex; gap: 0.5rem; justify-content: center;">
                <button
                  class="action-btn \${isRunning ? 'stop-btn' : 'start-btn'}"
                  onclick="toggleSkill('\${skill.id}', \${isRunning})"
                  style="padding: 0.25rem 0.75rem; border-radius: 4px; border: none; cursor: pointer; font-size: 0.875rem; font-weight: 500; transition: all 0.2s;">
                  \${isRunning ? '⏸️ Stop' : '▶️ Start'}
                </button>
              </div>
            \`;

            return \`
              <tr>
                <td>
                  <div class="skill-name">
                    <span class="skill-icon">🎯</span>
                    <div>
                      <div>\${skill.name}</div>
                      \${skill.version ? \`<small style="color: #a0aec0;">v\${skill.version}</small>\` : ''}
                    </div>
                  </div>
                </td>
                <td>
                  <div class="skill-description">\${skill.description}</div>
                  \${capabilities}
                  \${cronBadges}
                </td>
                <td>
                  <span class="status-badge \${skill.status}">\${skill.status}</span>
                </td>
                <td>
                  <div class="last-activated">\${lastActivated}</div>
                  \${usageDetails}
                </td>
                <td>
                  \${actionButtons}
                </td>
              </tr>
            \`;
          }).join('');
        }

        // Sort table
        function sortTable(column) {
          if (sortColumn === column) {
            sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
          } else {
            sortColumn = column;
            sortDirection = 'asc';
          }
          renderSkills();
        }

        // Format date
        function formatDate(date) {
          const now = new Date();
          const diff = now.getTime() - date.getTime();
          const days = Math.floor(diff / (1000 * 60 * 60 * 24));

          if (days === 0) {
            const hours = Math.floor(diff / (1000 * 60 * 60));
            if (hours === 0) {
              const minutes = Math.floor(diff / (1000 * 60));
              return \`\${minutes} minute\${minutes !== 1 ? 's' : ''} ago\`;
            }
            return \`\${hours} hour\${hours !== 1 ? 's' : ''} ago\`;
          } else if (days === 1) {
            return 'Yesterday';
          } else if (days < 7) {
            return \`\${days} days ago\`;
          } else {
            return date.toLocaleDateString('en-US', {
              month: 'short',
              day: 'numeric',
              year: 'numeric'
            });
          }
        }

        // Search handler
        document.getElementById('search').addEventListener('input', renderSkills);

        // Filter handlers
        document.querySelectorAll('.filter-btn').forEach(btn => {
          btn.addEventListener('click', (e) => {
            document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
            e.target.classList.add('active');
            currentFilter = e.target.dataset.filter;
            renderSkills();
          });
        });

        // Show/hide usage details
        function showUsageDetails(skillId) {
          const detailsEl = document.getElementById(\`usage-\${skillId}\`);
          if (detailsEl) {
            detailsEl.style.display = detailsEl.style.display === 'none' ? 'block' : 'none';
          }
        }

        // Toggle skill (start/stop)
        async function toggleSkill(skillId, isRunning) {
          try {
            const action = isRunning ? 'stop' : 'start';
            const response = await fetch(\`/api/skills/\${skillId}/\${action}\`, {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' }
            });

            if (response.ok) {
              const result = await response.json();
              alert(\`Skill \${action}ed successfully: \${result.message}\`);
              loadSkills(); // Reload to show updated status
            } else {
              const error = await response.json();
              alert(\`Failed to \${action} skill: \${error.message}\`);
            }
          } catch (error) {
            console.error('Error toggling skill:', error);
            alert('Failed to toggle skill. Please try again.');
          }
        }

        // Initial load
        loadSkills();

        // Auto-refresh DISABLED to prevent interference with product-import-from-url
        // (was causing issues when updating skills records)
      </script>
    </body>
    </html>
  `);
});

// API endpoint to get skills data
app.get('/api/skills', async (req: Request, res: Response) => {
  try {
    const skills = await scanSkills();
    res.json(skills);
  } catch (error) {
    console.error('Error getting skills:', error);
    res.status(500).json({ error: 'Failed to load skills' });
  }
});

// API endpoint to start a skill
app.post('/api/skills/:id/start', async (req: Request, res: Response) => {
  try {
    const skillId = req.params.id;
    const skillPath = path.join(CLAUDE_SKILLS_DIR, skillId);

    if (!fs.existsSync(skillPath)) {
      return res.status(404).json({ error: 'Skill not found' });
    }

    // Load skill configuration
    const skillJsonPath = path.join(skillPath, 'skill.json');
    if (!fs.existsSync(skillJsonPath)) {
      return res.status(400).json({ error: 'Skill configuration not found' });
    }

    const skillConfig = JSON.parse(fs.readFileSync(skillJsonPath, 'utf8'));

    // Update status to active
    skillConfig.status = 'active';
    fs.writeFileSync(skillJsonPath, JSON.stringify(skillConfig, null, 2));

    // Update records
    const records = loadRecords();
    const skillRecord = records.skills.find((s: any) => s.id === skillId);
    if (skillRecord) {
      skillRecord.status = 'active';
      skillRecord.lastActivated = new Date().toISOString();
      saveRecords(records.skills);
    }

    res.json({
      success: true,
      message: `Skill "${skillConfig.name}" started successfully`,
      skillId,
      status: 'active'
    });
  } catch (error) {
    console.error('Error starting skill:', error);
    res.status(500).json({ error: 'Failed to start skill' });
  }
});

// API endpoint to stop a skill
app.post('/api/skills/:id/stop', async (req: Request, res: Response) => {
  try {
    const skillId = req.params.id;
    const skillPath = path.join(CLAUDE_SKILLS_DIR, skillId);

    if (!fs.existsSync(skillPath)) {
      return res.status(404).json({ error: 'Skill not found' });
    }

    // Load skill configuration
    const skillJsonPath = path.join(skillPath, 'skill.json');
    if (!fs.existsSync(skillJsonPath)) {
      return res.status(400).json({ error: 'Skill configuration not found' });
    }

    const skillConfig = JSON.parse(fs.readFileSync(skillJsonPath, 'utf8'));

    // Update status to inactive
    skillConfig.status = 'inactive';
    fs.writeFileSync(skillJsonPath, JSON.stringify(skillConfig, null, 2));

    // Update records
    const records = loadRecords();
    const skillRecord = records.skills.find((s: any) => s.id === skillId);
    if (skillRecord) {
      skillRecord.status = 'inactive';
      saveRecords(records.skills);
    }

    res.json({
      success: true,
      message: `Skill "${skillConfig.name}" stopped successfully`,
      skillId,
      status: 'inactive'
    });
  } catch (error) {
    console.error('Error stopping skill:', error);
    res.status(500).json({ error: 'Failed to stop skill' });
  }
});

// Start server
app.listen(PORT, () => {
  console.log(`🎯 Skills Manager running on port ${PORT}`);
  console.log(`📊 Dashboard: http://45.61.58.125:${PORT}`);
  console.log(`🔐 Login: admin2025 / Otis`);
});