[object Object]

← back to Project Portfolio Viewer

untrack portfolio-viewer.ts.backup + broaden .gitignore (.backup/.pre-/.orig)

8198e0efa561fbf51bac26b6eef167d4e28d8135 · 2026-05-19 15:13:11 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 8198e0efa561fbf51bac26b6eef167d4e28d8135
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 19 15:13:11 2026 -0700

    untrack portfolio-viewer.ts.backup + broaden .gitignore (.backup/.pre-/.orig)
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 .gitignore                 |    5 +
 portfolio-viewer.ts.backup | 1133 --------------------------------------------
 2 files changed, 5 insertions(+), 1133 deletions(-)

diff --git a/.gitignore b/.gitignore
index e33f758..07e969f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,8 @@ tmp/
 .env.*.local
 .env.*
 *.bak
+*.bak.*
+*.backup
+*.backup.*
+*.pre-*
+*.orig
diff --git a/portfolio-viewer.ts.backup b/portfolio-viewer.ts.backup
deleted file mode 100644
index bcee478..0000000
--- a/portfolio-viewer.ts.backup
+++ /dev/null
@@ -1,1133 +0,0 @@
-/**
- * Project Portfolio Viewer
- *
- * Beautiful web interface to browse and explore all projects
- * Port: 7899
- */
-
-import express from 'express';
-import { promises as fs } from 'fs';
-import path from 'path';
-
-const app = express();
-const PORT = 7899;
-const PROJECTS_DIR = '/root/Projects';
-
-interface ProjectInfo {
-  name: string;
-  path: string;
-  displayName: string;
-  description: string;
-  type: string;
-  lastModified: Date;
-  size: number;
-  hasPackageJson: boolean;
-  hasIndexHtml: boolean;
-  technology: string[];
-  port?: number;
-  url?: string;
-}
-
-app.use(express.json());
-
-// Scan projects and gather metadata
-async function scanProjects(): Promise<ProjectInfo[]> {
-  const projects: ProjectInfo[] = [];
-
-  try {
-    const entries = await fs.readdir(PROJECTS_DIR, { withFileTypes: true });
-
-    for (const entry of entries) {
-      if (!entry.isDirectory()) continue;
-      if (entry.name.startsWith('.')) continue;
-      if (entry.name === 'project-portfolio-viewer') continue;
-
-      const projectPath = path.join(PROJECTS_DIR, entry.name);
-      const stats = await fs.stat(projectPath);
-
-      const port = await detectPort(projectPath, entry.name);
-      const url = port ? `http://45.61.58.125:${port}` : undefined;
-
-      const projectInfo: ProjectInfo = {
-        name: entry.name,
-        path: projectPath,
-        displayName: entry.name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
-        description: await getProjectDescription(projectPath),
-        type: await getProjectType(projectPath),
-        lastModified: stats.mtime,
-        size: await getDirectorySize(projectPath),
-        hasPackageJson: await fileExists(path.join(projectPath, 'package.json')),
-        hasIndexHtml: await fileExists(path.join(projectPath, 'index.html')),
-        technology: await detectTechnology(projectPath),
-        port,
-        url
-      };
-
-      projects.push(projectInfo);
-    }
-
-    return projects.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
-  } catch (error) {
-    console.error('Error scanning projects:', error);
-    return [];
-  }
-}
-
-async function getProjectDescription(projectPath: string): Promise<string> {
-  const packageJsonPath = path.join(projectPath, 'package.json');
-  try {
-    const content = await fs.readFile(packageJsonPath, 'utf-8');
-    const pkg = JSON.parse(content);
-    return pkg.description || pkg.name || 'No description available';
-  } catch {
-    return 'No description available';
-  }
-}
-
-async function getProjectType(projectPath: string): Promise<string> {
-  const hasPackageJson = await fileExists(path.join(projectPath, 'package.json'));
-  const hasIndexHtml = await fileExists(path.join(projectPath, 'index.html'));
-  const hasApp = await fileExists(path.join(projectPath, 'app'));
-
-  if (hasApp && hasPackageJson) return 'Next.js App';
-  if (hasPackageJson) return 'Node.js Project';
-  if (hasIndexHtml) return 'Static Website';
-  return 'General Project';
-}
-
-async function detectTechnology(projectPath: string): Promise<string[]> {
-  const tech: string[] = [];
-  const packageJsonPath = path.join(projectPath, 'package.json');
-
-  try {
-    const content = await fs.readFile(packageJsonPath, 'utf-8');
-    const pkg = JSON.parse(content);
-    const deps = { ...pkg.dependencies, ...pkg.devDependencies };
-
-    if (deps['next']) tech.push('Next.js');
-    if (deps['react']) tech.push('React');
-    if (deps['vue']) tech.push('Vue');
-    if (deps['express']) tech.push('Express');
-    if (deps['typescript']) tech.push('TypeScript');
-    if (deps['@capacitor/core']) tech.push('Capacitor');
-    if (deps['tailwindcss']) tech.push('Tailwind');
-  } catch {}
-
-  return tech;
-}
-
-async function fileExists(filePath: string): Promise<boolean> {
-  try {
-    await fs.access(filePath);
-    return true;
-  } catch {
-    return false;
-  }
-}
-
-async function detectPort(projectPath: string, projectName: string): Promise<number | undefined> {
-  // Try package.json first
-  const packageJsonPath = path.join(projectPath, 'package.json');
-  try {
-    const content = await fs.readFile(packageJsonPath, 'utf-8');
-    const pkg = JSON.parse(content);
-
-    // Check for port in package.json
-    if (pkg.port) return pkg.port;
-    if (pkg.config?.port) return pkg.config.port;
-  } catch {}
-
-  // Try to find PORT in main files
-  const mainFiles = ['index.ts', 'server.ts', 'app.ts', 'index.js', 'server.js', 'app.js'];
-
-  for (const file of mainFiles) {
-    const filePath = path.join(projectPath, file);
-    try {
-      const content = await fs.readFile(filePath, 'utf-8');
-
-      // Look for PORT = NUMBER or .listen(NUMBER
-      const portMatch = content.match(/(?:PORT\s*=\s*|listen\((?:process\.env\.PORT\s*\|\|\s*)?)([\d]+)/);
-      if (portMatch && portMatch[1]) {
-        return parseInt(portMatch[1]);
-      }
-    } catch {}
-  }
-
-  return undefined;
-}
-
-async function getDirectorySize(dirPath: string): Promise<number> {
-  let size = 0;
-  try {
-    const entries = await fs.readdir(dirPath, { withFileTypes: true });
-    for (const entry of entries) {
-      const entryPath = path.join(dirPath, entry.name);
-      if (entry.name === 'node_modules') continue;
-      if (entry.name === '.next') continue;
-
-      if (entry.isDirectory()) {
-        size += await getDirectorySize(entryPath);
-      } else {
-        const stats = await fs.stat(entryPath);
-        size += stats.size;
-      }
-    }
-  } catch {}
-  return size;
-}
-
-function formatBytes(bytes: number): string {
-  if (bytes === 0) return '0 Bytes';
-  const k = 1024;
-  const sizes = ['Bytes', 'KB', 'MB', 'GB'];
-  const i = Math.floor(Math.log(bytes) / Math.log(k));
-  return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
-}
-
-// API endpoint
-app.get('/api/projects', async (req, res) => {
-  const projects = await scanProjects();
-  res.json(projects);
-});
-
-// Main page
-app.get('/', async (req, res) => {
-  const projects = await scanProjects();
-
-  // Add cache-busting headers
-  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
-  res.setHeader('Pragma', 'no-cache');
-  res.setHeader('Expires', '0');
-  res.setHeader('Surrogate-Control', 'no-store');
-
-  const version = Date.now();
-
-  res.send(`
-<!DOCTYPE html>
-<html lang="en">
-<head>
-  <meta charset="UTF-8">
-  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
-  <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
-  <meta http-equiv="Pragma" content="no-cache">
-  <meta http-equiv="Expires" content="0">
-  <title>Project Portfolio Viewer v2.0 - Mobile Optimized</title>
-  <style data-version="${version}">
-    * {
-      margin: 0;
-      padding: 0;
-      box-sizing: border-box;
-    }
-
-    body {
-      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-      min-height: 100vh;
-      padding: 20px;
-    }
-
-    .container {
-      max-width: 1400px;
-      margin: 0 auto;
-    }
-
-    .header {
-      background: rgba(255, 255, 255, 0.95);
-      padding: 30px 20px;
-      border-radius: 15px;
-      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
-      margin-bottom: 20px;
-      text-align: center;
-    }
-
-    .header h1 {
-      font-size: 3em;
-      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-      -webkit-background-clip: text;
-      -webkit-text-fill-color: transparent;
-      margin-bottom: 10px;
-    }
-
-    .header p {
-      color: #666;
-      font-size: 1.2em;
-    }
-
-    .stats {
-      display: grid;
-      grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
-      gap: 15px;
-      margin-bottom: 20px;
-    }
-
-    .stat-card {
-      background: rgba(255, 255, 255, 0.95);
-      padding: 20px 15px;
-      border-radius: 12px;
-      box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
-      text-align: center;
-    }
-
-    .stat-card .number {
-      font-size: 2.5em;
-      font-weight: bold;
-      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-      -webkit-background-clip: text;
-      -webkit-text-fill-color: transparent;
-    }
-
-    .stat-card .label {
-      color: #666;
-      margin-top: 10px;
-      font-size: 1em;
-    }
-
-    .filters {
-      background: rgba(255, 255, 255, 0.95);
-      padding: 15px;
-      border-radius: 12px;
-      box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
-      margin-bottom: 20px;
-      display: flex;
-      gap: 10px;
-      flex-wrap: wrap;
-      align-items: center;
-    }
-
-    .filter-btn {
-      background: #f0f0f0;
-      border: 2px solid transparent;
-      padding: 8px 16px;
-      border-radius: 8px;
-      cursor: pointer;
-      font-weight: 600;
-      font-size: 0.9em;
-      transition: all 0.3s;
-    }
-
-    .filter-btn:hover {
-      background: #e0e0e0;
-    }
-
-    .filter-btn.active {
-      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-      color: white;
-    }
-
-    .search-box {
-      flex: 1;
-      min-width: 250px;
-    }
-
-    .search-box input {
-      width: 100%;
-      padding: 12px 20px;
-      border: 2px solid #ddd;
-      border-radius: 8px;
-      font-size: 1em;
-      transition: all 0.3s;
-    }
-
-    .search-box input:focus {
-      outline: none;
-      border-color: #667eea;
-    }
-
-    .projects-grid {
-      display: grid;
-      grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
-      gap: 25px;
-      margin-bottom: 40px;
-    }
-
-    .project-card {
-      background: rgba(255, 255, 255, 0.95);
-      border-radius: 20px;
-      overflow: hidden;
-      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
-      transition: all 0.3s;
-      cursor: pointer;
-    }
-
-    .project-card:hover {
-      transform: translateY(-10px);
-      box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
-    }
-
-    .project-header {
-      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-      padding: 25px;
-      color: white;
-    }
-
-    .project-title {
-      font-size: 1.5em;
-      font-weight: bold;
-      margin-bottom: 8px;
-    }
-
-    .project-type {
-      background: rgba(255, 255, 255, 0.2);
-      display: inline-block;
-      padding: 5px 12px;
-      border-radius: 12px;
-      font-size: 0.85em;
-    }
-
-    .project-body {
-      padding: 25px;
-    }
-
-    .project-description {
-      color: #666;
-      margin-bottom: 20px;
-      line-height: 1.6;
-    }
-
-    .project-tech {
-      display: flex;
-      flex-wrap: wrap;
-      gap: 8px;
-      margin-bottom: 20px;
-    }
-
-    .tech-badge {
-      background: #f0f0f0;
-      padding: 5px 12px;
-      border-radius: 12px;
-      font-size: 0.85em;
-      color: #667eea;
-      font-weight: 600;
-    }
-
-    .project-meta {
-      display: grid;
-      grid-template-columns: 1fr 1fr;
-      gap: 15px;
-      padding-top: 15px;
-      border-top: 1px solid #eee;
-    }
-
-    .meta-item {
-      display: flex;
-      align-items: center;
-      gap: 8px;
-      color: #666;
-      font-size: 0.9em;
-    }
-
-    .project-actions {
-      display: flex;
-      gap: 10px;
-      margin-top: 20px;
-    }
-
-    .action-btn {
-      flex: 1;
-      padding: 12px;
-      border: none;
-      border-radius: 8px;
-      font-weight: 600;
-      cursor: pointer;
-      transition: all 0.3s;
-    }
-
-    .btn-primary {
-      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-      color: white;
-    }
-
-    .btn-primary:hover {
-      transform: translateY(-2px);
-      box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
-    }
-
-    .btn-secondary {
-      background: #f0f0f0;
-      color: #333;
-    }
-
-    .btn-secondary:hover {
-      background: #e0e0e0;
-    }
-
-    @media (max-width: 768px) {
-      body {
-        padding: 4px;
-        background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
-      }
-
-      .header {
-        padding: 8px 10px;
-        margin-bottom: 8px;
-        border-radius: 8px;
-        background: rgba(255, 255, 255, 0.98);
-        border-left: 4px solid #e74c3c;
-      }
-
-      .header h1 {
-        font-size: 1.2em;
-        margin-bottom: 2px;
-        color: #e74c3c;
-      }
-
-      .header h1::after {
-        content: " 📱";
-        font-size: 0.8em;
-      }
-
-      .header p {
-        font-size: 0.75em;
-      }
-
-      .stats {
-        grid-template-columns: repeat(2, 1fr);
-        gap: 4px;
-        margin-bottom: 8px;
-      }
-
-      .stat-card {
-        padding: 8px 4px;
-        background: rgba(255, 255, 255, 0.98);
-      }
-
-      .stat-card .number {
-        font-size: 1.4em;
-        color: #e74c3c;
-        font-weight: 700;
-      }
-
-      .stat-card .label {
-        font-size: 0.7em;
-        margin-top: 2px;
-      }
-
-      .filters {
-        padding: 8px;
-        gap: 4px;
-        margin-bottom: 8px;
-        display: block;
-        background: rgba(255, 255, 255, 0.98);
-      }
-
-      .filter-btn {
-        display: inline-block;
-        padding: 4px 8px;
-        font-size: 0.75em;
-        margin: 2px;
-        background: #e74c3c;
-        color: white;
-        border: 1px solid #c0392b;
-      }
-
-      .filter-btn.active {
-        background: #c0392b;
-      }
-
-      .search-box {
-        width: 100%;
-        margin-top: 8px;
-        min-width: 100%;
-      }
-
-      .search-box input {
-        width: 100%;
-        padding: 12px 15px;
-        font-size: 16px;
-        border-radius: 8px;
-        -webkit-appearance: none;
-        border: 2px solid #e74c3c;
-        background: white;
-      }
-
-      .search-box input:focus {
-        outline: none;
-        border-color: #c0392b;
-        box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.2);
-      }
-
-      .projects-grid {
-        grid-template-columns: 1fr;
-        gap: 8px;
-      }
-
-      .project-card {
-        border-radius: 8px;
-        border: 2px solid rgba(231, 76, 60, 0.3);
-      }
-
-      .project-header {
-        padding: 10px;
-        background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
-      }
-
-      .project-title {
-        font-size: 1.1em;
-        color: white;
-      }
-
-      .project-type {
-        font-size: 0.7em;
-        padding: 3px 6px;
-        background: rgba(255, 255, 255, 0.3);
-        color: white;
-      }
-
-      .project-body {
-        padding: 10px;
-      }
-
-      .project-description {
-        font-size: 0.85em;
-        margin-bottom: 12px;
-      }
-
-      .tech-badge {
-        font-size: 0.75em;
-        padding: 4px 10px;
-      }
-
-      .meta-item {
-        font-size: 0.8em;
-      }
-
-      .action-btn {
-        padding: 10px;
-        font-size: 0.9em;
-      }
-
-      .controls {
-        flex-direction: column;
-        width: 100%;
-        gap: 8px;
-      }
-
-      .btn {
-        width: 100%;
-        padding: 10px;
-        font-size: 0.9em;
-      }
-    }
-  </style>
-</head>
-<body>
-  <div class="container">
-    <div class="header">
-      <h1>🚀 Project Portfolio</h1>
-      <p>Browse and explore all your projects <small style="opacity: 0.6;">v${version}</small></p>
-    </div>
-
-    <div class="stats">
-      <div class="stat-card">
-        <div class="number">${projects.length}</div>
-        <div class="label">Total Projects</div>
-      </div>
-      <div class="stat-card">
-        <div class="number">${projects.filter(p => p.type.includes('Next.js')).length}</div>
-        <div class="label">Next.js Apps</div>
-      </div>
-      <div class="stat-card">
-        <div class="number">${projects.filter(p => p.hasPackageJson).length}</div>
-        <div class="label">Node Projects</div>
-      </div>
-      <div class="stat-card">
-        <div class="number">${projects.filter(p => p.hasIndexHtml && !p.hasPackageJson).length}</div>
-        <div class="label">Static Sites</div>
-      </div>
-    </div>
-
-    <div class="filters">
-      <button class="filter-btn active" onclick="filterProjects('all')">All Projects</button>
-      <button class="filter-btn" onclick="filterProjects('nextjs')">Next.js</button>
-      <button class="filter-btn" onclick="filterProjects('nodejs')">Node.js</button>
-      <button class="filter-btn" onclick="filterProjects('static')">Static</button>
-      <div class="search-box">
-        <input type="text" id="searchInput" placeholder="🔍 Search projects..." oninput="searchProjects()">
-      </div>
-    </div>
-
-    <div class="projects-grid" id="projectsGrid">
-      ${projects.map(project => `
-        <div class="project-card" data-type="${project.type}" data-name="${project.name.toLowerCase()}">
-          <div class="project-header">
-            <div class="project-title">${project.displayName}</div>
-            <span class="project-type">${project.type}</span>
-          </div>
-          <div class="project-body">
-            <div class="project-description">${project.description}</div>
-            ${project.technology.length > 0 ? `
-              <div class="project-tech">
-                ${project.technology.map(tech => `<span class="tech-badge">${tech}</span>`).join('')}
-              </div>
-            ` : ''}
-            <div class="project-meta">
-              <div class="meta-item">
-                📅 ${new Date(project.lastModified).toLocaleDateString()}
-              </div>
-              <div class="meta-item">
-                💾 ${formatBytes(project.size)}
-              </div>
-            </div>
-            <div class="project-actions">
-              ${project.url ? `<button class="action-btn btn-primary" onclick="window.open('${project.url}', '_blank')">
-                🚀 Open Project ${project.port ? `(Port ${project.port})` : ''}
-              </button>` : '<button class="action-btn" disabled style="opacity: 0.5;">No URL Found</button>'}
-              <button class="action-btn btn-secondary" onclick="viewDetails('${project.name}')">
-                Details
-              </button>
-            </div>
-          </div>
-        </div>
-      `).join('')}
-    </div>
-  </div>
-
-  <script>
-    function filterProjects(type) {
-      document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
-      event.target.classList.add('active');
-
-      const cards = document.querySelectorAll('.project-card');
-      cards.forEach(card => {
-        const cardType = card.dataset.type.toLowerCase();
-        if (type === 'all') {
-          card.style.display = 'block';
-        } else if (type === 'nextjs' && cardType.includes('next.js')) {
-          card.style.display = 'block';
-        } else if (type === 'nodejs' && cardType.includes('node.js')) {
-          card.style.display = 'block';
-        } else if (type === 'static' && cardType.includes('static')) {
-          card.style.display = 'block';
-        } else {
-          card.style.display = 'none';
-        }
-      });
-    }
-
-    function searchProjects() {
-      const searchTerm = document.getElementById('searchInput').value.toLowerCase();
-      const cards = document.querySelectorAll('.project-card');
-
-      cards.forEach(card => {
-        const name = card.dataset.name;
-        if (name.includes(searchTerm)) {
-          card.style.display = 'block';
-        } else {
-          card.style.display = 'none';
-        }
-      });
-    }
-
-    function viewDetails(name) {
-      window.location.href = '/project/' + name;
-    }
-  </script>
-</body>
-</html>
-  `);
-});
-
-// Individual project details page
-app.get('/project/:name', async (req, res) => {
-  const projectName = req.params.name;
-  const projects = await scanProjects();
-  const project = projects.find(p => p.name === projectName);
-
-  if (!project) {
-    res.status(404).send('Project not found');
-    return;
-  }
-
-  res.send(`
-<!DOCTYPE html>
-<html lang="en">
-<head>
-  <meta charset="UTF-8">
-  <meta name="viewport" content="width=device-width, initial-scale=1.0">
-  <title>${project.displayName} - Project Details</title>
-  <style>
-    * { margin: 0; padding: 0; box-sizing: border-box; }
-    body {
-      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
-      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-      min-height: 100vh;
-      padding: 40px 20px;
-    }
-    .container {
-      max-width: 1000px;
-      margin: 0 auto;
-      background: rgba(255, 255, 255, 0.95);
-      border-radius: 20px;
-      padding: 40px;
-      box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
-    }
-    .back-btn {
-      display: inline-block;
-      padding: 10px 20px;
-      background: #f0f0f0;
-      border-radius: 8px;
-      text-decoration: none;
-      color: #333;
-      margin-bottom: 30px;
-      transition: all 0.3s;
-    }
-    .back-btn:hover {
-      background: #e0e0e0;
-    }
-    h1 {
-      font-size: 2.5em;
-      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-      -webkit-background-clip: text;
-      -webkit-text-fill-color: transparent;
-      margin-bottom: 20px;
-    }
-    .detail-grid {
-      display: grid;
-      grid-template-columns: 200px 1fr;
-      gap: 20px;
-      margin-top: 30px;
-    }
-    .detail-label {
-      font-weight: bold;
-      color: #667eea;
-    }
-    .detail-value {
-      color: #666;
-    }
-    .tech-list {
-      display: flex;
-      gap: 10px;
-      flex-wrap: wrap;
-    }
-    .tech-badge {
-      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-      color: white;
-      padding: 8px 16px;
-      border-radius: 12px;
-      font-weight: 600;
-    }
-  </style>
-</head>
-<body>
-  <div class="container">
-    <a href="/" class="back-btn">← Back to Projects</a>
-    <h1>${project.displayName}</h1>
-    <div class="detail-grid">
-      <div class="detail-label">Project Name:</div>
-      <div class="detail-value">${project.name}</div>
-
-      <div class="detail-label">Type:</div>
-      <div class="detail-value">${project.type}</div>
-
-      <div class="detail-label">Description:</div>
-      <div class="detail-value">${project.description}</div>
-
-      <div class="detail-label">Path:</div>
-      <div class="detail-value">${project.path}</div>
-
-      <div class="detail-label">Last Modified:</div>
-      <div class="detail-value">${new Date(project.lastModified).toLocaleString()}</div>
-
-      <div class="detail-label">Size:</div>
-      <div class="detail-value">${formatBytes(project.size)}</div>
-
-      <div class="detail-label">Technologies:</div>
-      <div class="tech-list">
-        ${project.technology.length > 0
-          ? project.technology.map(tech => `<span class="tech-badge">${tech}</span>`).join('')
-          : '<span class="detail-value">None detected</span>'}
-      </div>
-
-      <div class="detail-label">Has package.json:</div>
-      <div class="detail-value">${project.hasPackageJson ? '✅ Yes' : '❌ No'}</div>
-
-      <div class="detail-label">Has index.html:</div>
-      <div class="detail-value">${project.hasIndexHtml ? '✅ Yes' : '❌ No'}</div>
-    </div>
-  </div>
-</body>
-</html>
-  `);
-});
-
-// Health check
-// Mobile-specific endpoint - NEW URL to bypass cache completely
-app.get('/mobile', async (req, res) => {
-  const projects = await scanProjects();
-
-  // Add cache-busting headers
-  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
-  res.setHeader('Pragma', 'no-cache');
-  res.setHeader('Expires', '0');
-  res.setHeader('Surrogate-Control', 'no-store');
-
-  const version = Date.now();
-
-  // Force mobile view by directly applying mobile styles (no media query)
-  res.send(`
-<!DOCTYPE html>
-<html lang="en">
-<head>
-  <meta charset="UTF-8">
-  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
-  <title>Project Portfolio - Mobile View v${version}</title>
-  <style>
-    * {
-      margin: 0;
-      padding: 0;
-      box-sizing: border-box;
-    }
-
-    body {
-      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
-      background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
-      min-height: 100vh;
-      padding: 4px;
-    }
-
-    .header {
-      background: rgba(255, 255, 255, 0.98);
-      padding: 8px 10px;
-      margin-bottom: 8px;
-      border-radius: 8px;
-      border-left: 4px solid #e74c3c;
-      text-align: center;
-    }
-
-    .header h1 {
-      font-size: 1.2em;
-      margin-bottom: 2px;
-      color: #e74c3c;
-    }
-
-    .header p {
-      font-size: 0.75em;
-      color: #666;
-    }
-
-    .notice {
-      background: #fff3cd;
-      border: 2px solid #ffc107;
-      padding: 12px;
-      margin-bottom: 10px;
-      border-radius: 8px;
-      text-align: center;
-      font-size: 0.9em;
-    }
-
-    .stats {
-      display: grid;
-      grid-template-columns: repeat(2, 1fr);
-      gap: 4px;
-      margin-bottom: 8px;
-    }
-
-    .stat-card {
-      background: rgba(255, 255, 255, 0.98);
-      padding: 8px 4px;
-      border-radius: 8px;
-      text-align: center;
-    }
-
-    .stat-card .number {
-      font-size: 1.4em;
-      font-weight: 700;
-      color: #e74c3c;
-    }
-
-    .stat-card .label {
-      font-size: 0.7em;
-      color: #666;
-      margin-top: 2px;
-    }
-
-    .filters {
-      background: rgba(255, 255, 255, 0.98);
-      padding: 8px;
-      margin-bottom: 8px;
-      border-radius: 8px;
-    }
-
-    .filter-btn {
-      display: inline-block;
-      padding: 4px 8px;
-      margin: 2px;
-      background: #e74c3c;
-      color: white;
-      border: 1px solid #c0392b;
-      border-radius: 6px;
-      cursor: pointer;
-      font-size: 0.75em;
-    }
-
-    .filter-btn.active {
-      background: #c0392b;
-    }
-
-    .search-box {
-      width: 100%;
-      margin-top: 8px;
-    }
-
-    .search-box input {
-      width: 100%;
-      padding: 12px 15px;
-      font-size: 16px;
-      border: 2px solid #e74c3c;
-      border-radius: 8px;
-      background: white;
-    }
-
-    .projects-grid {
-      display: grid;
-      grid-template-columns: 1fr;
-      gap: 8px;
-    }
-
-    .project-card {
-      background: white;
-      border-radius: 8px;
-      overflow: hidden;
-      border: 2px solid rgba(231, 76, 60, 0.3);
-    }
-
-    .project-header {
-      background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
-      padding: 10px;
-      color: white;
-    }
-
-    .project-title {
-      font-size: 1.1em;
-      font-weight: 600;
-      margin-bottom: 4px;
-    }
-
-    .project-type {
-      display: inline-block;
-      background: rgba(255, 255, 255, 0.3);
-      padding: 3px 6px;
-      border-radius: 4px;
-      font-size: 0.7em;
-    }
-
-    .project-body {
-      padding: 10px;
-    }
-
-    .project-description {
-      font-size: 0.85em;
-      color: #666;
-      margin-bottom: 8px;
-    }
-
-    .tech-badges {
-      display: flex;
-      flex-wrap: wrap;
-      gap: 4px;
-      margin-bottom: 8px;
-    }
-
-    .tech-badge {
-      background: #f0f0f0;
-      padding: 3px 8px;
-      border-radius: 4px;
-      font-size: 0.7em;
-    }
-
-    .project-meta {
-      font-size: 0.75em;
-      color: #999;
-      margin-bottom: 8px;
-    }
-
-    .action-btn {
-      background: #e74c3c;
-      color: white;
-      padding: 10px;
-      border-radius: 6px;
-      text-align: center;
-      font-size: 0.9em;
-      font-weight: 600;
-    }
-  </style>
-</head>
-<body>
-  <div class="header">
-    <h1>📱 Project Portfolio MOBILE</h1>
-    <p>v${version}</p>
-  </div>
-
-  <div class="notice">
-    🎯 This is the MOBILE-OPTIMIZED view with RED theme
-  </div>
-
-  <div class="stats">
-    <div class="stat-card">
-      <div class="number">${projects.length}</div>
-      <div class="label">Total Projects</div>
-    </div>
-    <div class="stat-card">
-      <div class="number">${projects.filter((p: any) => p.hasPackageJson).length}</div>
-      <div class="label">Node.js Apps</div>
-    </div>
-  </div>
-
-  <div class="filters">
-    <button class="filter-btn active">All</button>
-    <button class="filter-btn">Node.js</button>
-    <button class="filter-btn">HTML</button>
-    <div class="search-box">
-      <input type="text" placeholder="Search projects...">
-    </div>
-  </div>
-
-  <div class="projects-grid">
-    ${projects.slice(0, 5).map((p: any) => `
-      <div class="project-card">
-        <div class="project-header">
-          <div class="project-title">${p.displayName}</div>
-          <span class="project-type">${p.type}</span>
-        </div>
-        <div class="project-body">
-          <div class="project-description">${p.description || 'No description'}</div>
-          <div class="tech-badges">
-            ${p.technology.map((t: string) => `<span class="tech-badge">${t}</span>`).join('')}
-          </div>
-          <div class="project-meta">
-            Last modified: ${new Date(p.lastModified).toLocaleDateString()}
-          </div>
-          <div class="action-btn">View Details</div>
-        </div>
-      </div>
-    `).join('')}
-  </div>
-
-  <div class="notice" style="margin-top: 10px;">
-    If you see RED background and this notice, the mobile view is working! ✅
-  </div>
-</body>
-</html>
-  `);
-});
-
-app.get('/api/health', (req, res) => {
-  res.json({
-    status: 'online',
-    port: PORT,
-    uptime: process.uptime(),
-    projectsDir: PROJECTS_DIR
-  });
-});
-
-app.listen(PORT, '0.0.0.0', () => {
-  console.log('');
-  console.log('🚀 Project Portfolio Viewer');
-  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
-  console.log(`🌍 External: http://45.61.58.125:${PORT}`);
-  console.log(`🏠 Local: http://localhost:${PORT}`);
-  console.log('');
-  console.log(`📁 Projects Directory: ${PROJECTS_DIR}`);
-  console.log('✅ Portfolio viewer ready...');
-  console.log('');
-});

← 763b975 initial scaffold (gitify-all 2026-05-06)  ·  back to Project Portfolio Viewer  ·  add sort+density controls, .bak/.pre- 404 middleware, noopen c9c5cf2 →