← 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
M .gitignoreD portfolio-viewer.ts.backup
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 →