First Init
Deploy to Production / Build & Verify (push) Failing after 5m56s
Ping Search Engines / Notify Search Engines (push) Successful in 2s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Smoke Tests (P0) (push) Failing after 11m26s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m42s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m2s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 16m14s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 17m45s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 25m23s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 20s
E2E Test Suite / Mobile Device Tests (push) Failing after 2h49m9s
Uptime Monitor / Health & Response Time (push) Failing after 2s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Failing after 3s
Uptime Monitor / Record Uptime Success (push) Has been skipped
Deploy to Production / Build & Verify (push) Failing after 5m56s
Ping Search Engines / Notify Search Engines (push) Successful in 2s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Smoke Tests (P0) (push) Failing after 11m26s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m42s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m2s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 16m14s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 17m45s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 25m23s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 20s
E2E Test Suite / Mobile Device Tests (push) Failing after 2h49m9s
Uptime Monitor / Health & Response Time (push) Failing after 2s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Failing after 3s
Uptime Monitor / Record Uptime Success (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
# Performance Testing Scripts
|
||||
|
||||
Quick reference for performance testing and monitoring.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Test Current Server Performance
|
||||
```bash
|
||||
# Quick check (recommended)
|
||||
curl -w "Time: %{time_total}s\n" -o /dev/null -s http://localhost:10000/
|
||||
|
||||
# Comprehensive test
|
||||
python scripts/performance_test.py
|
||||
|
||||
# Shell script (Unix/Mac)
|
||||
bash scripts/quick-perf-check.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Available Scripts
|
||||
|
||||
### 1. `performance_test.py`
|
||||
**Purpose:** Comprehensive Python-based performance testing
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
python scripts/performance_test.py
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Tests multiple endpoints (pages + APIs)
|
||||
- Statistical analysis (mean, median, std dev)
|
||||
- Domain verification
|
||||
- Performance recommendations
|
||||
- JSON output support
|
||||
|
||||
**Output:**
|
||||
- Response time statistics per endpoint
|
||||
- Overall performance summary
|
||||
- Recommendations for slow endpoints
|
||||
- Pass/fail status
|
||||
|
||||
---
|
||||
|
||||
### 2. `quick-perf-check.sh`
|
||||
**Purpose:** Fast performance spot-check
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Default (localhost:10000)
|
||||
bash scripts/quick-perf-check.sh
|
||||
|
||||
# Custom URL
|
||||
bash scripts/quick-perf-check.sh https://workroot.in
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Quick response time checks
|
||||
- Color-coded status indicators
|
||||
- Minimal dependencies (just curl)
|
||||
- Production-ready
|
||||
|
||||
---
|
||||
|
||||
### 3. `lighthouse_audit.py`
|
||||
**Purpose:** Lighthouse performance audit
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
python scripts/lighthouse_audit.py <url>
|
||||
|
||||
# Example
|
||||
python scripts/lighthouse_audit.py http://localhost:10000
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Full Lighthouse audit
|
||||
- Performance, accessibility, SEO scores
|
||||
- Best practices analysis
|
||||
- Detailed issue reporting
|
||||
|
||||
**Note:** May have encoding issues on Windows. Use WSL or fix Unicode output.
|
||||
|
||||
---
|
||||
|
||||
## Performance Targets
|
||||
|
||||
| Metric | Excellent | Good | Needs Work |
|
||||
|--------|-----------|------|------------|
|
||||
| **SSR Page Load** | < 200ms | < 500ms | > 500ms |
|
||||
| **API Response** | < 100ms | < 200ms | > 200ms |
|
||||
| **TTFB** | < 200ms | < 300ms | > 300ms |
|
||||
|
||||
---
|
||||
|
||||
## Core Web Vitals (Production)
|
||||
|
||||
| Metric | Good | Needs Improvement | Poor |
|
||||
|--------|------|-------------------|------|
|
||||
| **LCP** | < 2.5s | 2.5s - 4.0s | > 4.0s |
|
||||
| **INP** | < 200ms | 200ms - 500ms | > 500ms |
|
||||
| **CLS** | < 0.1 | 0.1 - 0.25 | > 0.25 |
|
||||
|
||||
---
|
||||
|
||||
## Manual Testing with curl
|
||||
|
||||
### Basic Response Time
|
||||
```bash
|
||||
curl -w "\nTime: %{time_total}s\n" -o /dev/null -s http://localhost:10000/
|
||||
```
|
||||
|
||||
### Detailed Timing
|
||||
```bash
|
||||
curl -w "\n
|
||||
Total Time: %{time_total}s
|
||||
DNS Lookup: %{time_namelookup}s
|
||||
TCP Connect: %{time_connect}s
|
||||
TLS Handshake: %{time_appconnect}s
|
||||
Time to First Byte: %{time_starttransfer}s
|
||||
HTTP Code: %{http_code}
|
||||
\n" -o /dev/null -s http://localhost:10000/
|
||||
```
|
||||
|
||||
### Test Multiple Endpoints
|
||||
```bash
|
||||
for endpoint in / /contact /about /api/health.json; do
|
||||
echo "Testing $endpoint"
|
||||
curl -w "Time: %{time_total}s\n" -o /dev/null -s http://localhost:10000$endpoint
|
||||
echo ""
|
||||
done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions Example
|
||||
```yaml
|
||||
- name: Performance Test
|
||||
run: |
|
||||
npm run build
|
||||
npm run preview &
|
||||
sleep 5
|
||||
python scripts/performance_test.py
|
||||
```
|
||||
|
||||
### Pre-deployment Check
|
||||
```bash
|
||||
# Build and test
|
||||
npm run build
|
||||
npm run preview &
|
||||
sleep 5
|
||||
python scripts/performance_test.py
|
||||
|
||||
# If all tests pass, deploy
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Performance tests passed"
|
||||
# Deploy command here
|
||||
else
|
||||
echo "Performance tests failed"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Slow Response Times (> 500ms)
|
||||
|
||||
**Possible Causes:**
|
||||
1. Development server (not optimized)
|
||||
2. Cold start (first request)
|
||||
3. Large bundle size
|
||||
4. Unoptimized images
|
||||
5. Missing caching
|
||||
|
||||
**Solutions:**
|
||||
```bash
|
||||
# 1. Build for production
|
||||
npm run build
|
||||
|
||||
# 2. Analyze bundle
|
||||
npm run build -- --analyze
|
||||
|
||||
# 3. Check bundle size
|
||||
du -sh dist/
|
||||
|
||||
# 4. Test production build
|
||||
npm run preview
|
||||
```
|
||||
|
||||
### High Variance in Response Times
|
||||
|
||||
**Possible Causes:**
|
||||
1. Network issues
|
||||
2. System load
|
||||
3. Garbage collection
|
||||
4. Cache inconsistency
|
||||
|
||||
**Solutions:**
|
||||
- Run multiple iterations (10+)
|
||||
- Test during low system load
|
||||
- Close other applications
|
||||
- Clear caches between tests
|
||||
|
||||
---
|
||||
|
||||
## Monitoring in Production
|
||||
|
||||
### Recommended Tools
|
||||
- **DataDog RUM:** Real User Monitoring
|
||||
- **New Relic:** Application Performance Monitoring
|
||||
- **Google Analytics:** Core Web Vitals
|
||||
- **Sentry:** Performance tracking + errors
|
||||
|
||||
### Custom Performance Monitoring
|
||||
```javascript
|
||||
// Add to your app
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('load', () => {
|
||||
const perfData = performance.getEntriesByType('navigation')[0];
|
||||
console.log('Page Load Time:', perfData.loadEventEnd - perfData.fetchStart, 'ms');
|
||||
console.log('DOM Interactive:', perfData.domInteractive - perfData.fetchStart, 'ms');
|
||||
console.log('TTFB:', perfData.responseStart - perfData.requestStart, 'ms');
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Test Regularly:** Run performance tests before each deployment
|
||||
2. **Set Baselines:** Document baseline performance metrics
|
||||
3. **Monitor Trends:** Track performance over time
|
||||
4. **Test Production:** Dev server performance != production performance
|
||||
5. **Real Users:** Use RUM to understand actual user experience
|
||||
6. **Set Budgets:** Define performance budgets and enforce them
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [Web.dev Performance](https://web.dev/performance/)
|
||||
- [Lighthouse Scoring](https://web.dev/performance-scoring/)
|
||||
- [Core Web Vitals](https://web.dev/vitals/)
|
||||
- [Astro Performance](https://docs.astro.build/en/guides/performance/)
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
# Test homepage
|
||||
curl -w "%{time_total}s\n" -o /dev/null -s http://localhost:10000/
|
||||
|
||||
# Comprehensive test
|
||||
python scripts/performance_test.py
|
||||
|
||||
# Lighthouse audit
|
||||
npx lighthouse http://localhost:10000 --view
|
||||
|
||||
# Bundle analysis
|
||||
npm run build -- --analyze
|
||||
```
|
||||
@@ -0,0 +1,119 @@
|
||||
# Backup Scripts
|
||||
|
||||
Automated backup system for WorkRoot website.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# First-time setup
|
||||
mkdir -p backups/{daily,weekly,monthly,pre-deploy,incremental,safety}
|
||||
|
||||
# Create your first backup
|
||||
npm run backup:full
|
||||
|
||||
# Verify it worked
|
||||
npm run backup:verify
|
||||
```
|
||||
|
||||
## Available Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `npm run backup:full` | Full backup of all critical files |
|
||||
| `npm run backup:content` | Quick backup of content only |
|
||||
| `npm run backup:config` | Backup configs and env files |
|
||||
| `npm run backup:verify` | Check backup health and integrity |
|
||||
| `npm run backup:list` | List all available backups |
|
||||
| `npm run backup:restore` | Restore from backup (interactive) |
|
||||
| `npm run backup:cleanup` | Remove old backups per retention policy |
|
||||
|
||||
## Automation Setup
|
||||
|
||||
### Linux/macOS (cron)
|
||||
|
||||
```bash
|
||||
# Edit crontab
|
||||
crontab -e
|
||||
|
||||
# Daily backup at 2 AM
|
||||
0 2 * * * cd /path/to/project && npm run backup:full
|
||||
|
||||
# Cleanup weekly on Sunday at 3 AM
|
||||
0 3 * * 0 cd /path/to/project && npm run backup:cleanup
|
||||
```
|
||||
|
||||
### Windows (Task Scheduler)
|
||||
|
||||
```powershell
|
||||
# Daily backup
|
||||
schtasks /create /tn "WorkRoot-DailyBackup" /tr "npm run backup:full" /sc daily /st 02:00
|
||||
|
||||
# Weekly cleanup
|
||||
schtasks /create /tn "WorkRoot-CleanupBackup" /tr "npm run backup:cleanup" /sc weekly /d SUN /st 03:00
|
||||
```
|
||||
|
||||
## Restore Examples
|
||||
|
||||
```bash
|
||||
# List available backups
|
||||
npm run backup:list
|
||||
|
||||
# Restore from latest
|
||||
npm run backup:restore backups/daily/latest-full.tar.gz
|
||||
|
||||
# Verify specific backup
|
||||
npm run backup:verify backups/daily/full-backup-2026-03-21.tar.gz
|
||||
```
|
||||
|
||||
## Storage Structure
|
||||
|
||||
```
|
||||
backups/
|
||||
├── daily/ # 7 days retention
|
||||
├── weekly/ # 4 weeks retention
|
||||
├── monthly/ # 3 months retention
|
||||
├── incremental/ # 72 hours retention
|
||||
├── pre-deploy/ # Last 10 deployments
|
||||
└── safety/ # Pre-restore backups (30 days)
|
||||
```
|
||||
|
||||
## Pre-Deployment
|
||||
|
||||
Always backup before deployment:
|
||||
|
||||
```bash
|
||||
# Manual
|
||||
npm run backup:config
|
||||
|
||||
# Automatic (in deployment script)
|
||||
npm run deploy # Includes automatic config backup
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Backup fails
|
||||
|
||||
```bash
|
||||
# Check disk space
|
||||
df -h
|
||||
|
||||
# Check permissions
|
||||
ls -la backups/
|
||||
|
||||
# Make scripts executable
|
||||
chmod +x scripts/backup/*.sh
|
||||
```
|
||||
|
||||
### Restore fails
|
||||
|
||||
```bash
|
||||
# Verify backup integrity
|
||||
npm run backup:verify backups/daily/backup-file.tar.gz
|
||||
|
||||
# Check backup contents
|
||||
tar -tzf backups/daily/backup-file.tar.gz
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
See [BACKUP_STRATEGY.md](../../.agents/devops-engineer/BACKUP_STRATEGY.md) for complete documentation.
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
# Configuration Backup Script for WorkRoot Website
|
||||
# Backs up critical config files and environment variables
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
BACKUP_DIR="$PROJECT_ROOT/backups/pre-deploy"
|
||||
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
|
||||
BACKUP_FILE="$BACKUP_DIR/config-$TIMESTAMP.tar.gz"
|
||||
LOG_FILE="$PROJECT_ROOT/backups/backup.log"
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo "[$(date +"%Y-%m-%d %H:%M:%S")] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
log "${GREEN}Starting configuration backup...${NC}"
|
||||
|
||||
# Configuration files to backup
|
||||
CONFIG_FILES=(
|
||||
"astro.config.mjs"
|
||||
"tailwind.config.mjs"
|
||||
"playwright.config.ts"
|
||||
"tsconfig.json"
|
||||
"package.json"
|
||||
"package-lock.json"
|
||||
".env"
|
||||
".env.example"
|
||||
"src/middleware.ts"
|
||||
"src/content/config.ts"
|
||||
)
|
||||
|
||||
# Filter out non-existent files
|
||||
EXISTING_FILES=()
|
||||
for file in "${CONFIG_FILES[@]}"; do
|
||||
if [ -e "$PROJECT_ROOT/$file" ]; then
|
||||
EXISTING_FILES+=("$file")
|
||||
fi
|
||||
done
|
||||
|
||||
# Create backup
|
||||
cd "$PROJECT_ROOT"
|
||||
tar -czf "$BACKUP_FILE" "${EXISTING_FILES[@]}" 2>/dev/null
|
||||
|
||||
# Verify
|
||||
if [ ! -f "$BACKUP_FILE" ]; then
|
||||
log "ERROR: Config backup failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
||||
log "${GREEN}Configuration backup completed${NC}"
|
||||
log "Location: $BACKUP_FILE"
|
||||
log "Size: $BACKUP_SIZE"
|
||||
|
||||
# Create latest symlink
|
||||
ln -sf "$BACKUP_FILE" "$BACKUP_DIR/latest-config.tar.gz"
|
||||
|
||||
# Cleanup (keep last 10 config backups)
|
||||
cd "$BACKUP_DIR"
|
||||
ls -t config-*.tar.gz 2>/dev/null | tail -n +11 | xargs -r rm -f
|
||||
log "Old config backups cleaned up (kept last 10)"
|
||||
|
||||
echo -e "${GREEN}✓ Configuration backup completed${NC}"
|
||||
echo -e " Location: $BACKUP_FILE"
|
||||
echo -e " Size: $BACKUP_SIZE"
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
# Content-Only Backup Script for WorkRoot Website
|
||||
# Quick incremental backup of content files (blog posts, markdown)
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
BACKUP_DIR="$PROJECT_ROOT/backups/incremental"
|
||||
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
|
||||
BACKUP_FILE="$BACKUP_DIR/content-$TIMESTAMP.tar.gz"
|
||||
LOG_FILE="$PROJECT_ROOT/backups/backup.log"
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo "[$(date +"%Y-%m-%d %H:%M:%S")] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
log "${GREEN}Starting content backup...${NC}"
|
||||
|
||||
# Content directories to backup
|
||||
CONTENT_ITEMS=(
|
||||
"src/content/"
|
||||
"public/images/"
|
||||
"public/assets/"
|
||||
)
|
||||
|
||||
# Create backup
|
||||
cd "$PROJECT_ROOT"
|
||||
tar -czf "$BACKUP_FILE" "${CONTENT_ITEMS[@]}" 2>/dev/null || {
|
||||
log "${YELLOW}Warning: Some content directories may not exist${NC}"
|
||||
}
|
||||
|
||||
# Verify backup
|
||||
if [ ! -f "$BACKUP_FILE" ]; then
|
||||
log "ERROR: Content backup failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
||||
log "${GREEN}Content backup completed${NC}"
|
||||
log "Location: $BACKUP_FILE"
|
||||
log "Size: $BACKUP_SIZE"
|
||||
|
||||
# Create latest symlink
|
||||
ln -sf "$BACKUP_FILE" "$BACKUP_DIR/latest-content.tar.gz"
|
||||
|
||||
# Cleanup old incremental backups (keep last 72 hours)
|
||||
find "$BACKUP_DIR" -name "content-*.tar.gz" -type f -mtime +3 -delete 2>/dev/null || true
|
||||
log "Old backups cleaned up"
|
||||
|
||||
echo -e "${GREEN}✓ Content backup completed${NC}"
|
||||
echo -e " Location: $BACKUP_FILE"
|
||||
echo -e " Size: $BACKUP_SIZE"
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/bin/bash
|
||||
# Full Backup Script for WorkRoot Website
|
||||
# Backs up all critical files: content, source, configs, environment
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Configuration
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
BACKUP_DIR="$PROJECT_ROOT/backups/daily"
|
||||
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
|
||||
BACKUP_FILE="$BACKUP_DIR/full-backup-$TIMESTAMP.tar.gz"
|
||||
LOG_FILE="$PROJECT_ROOT/backups/backup.log"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
echo "[$(date +"%Y-%m-%d %H:%M:%S")] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Create backup directory if it doesn't exist
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
log "${GREEN}Starting full backup...${NC}"
|
||||
|
||||
# Files and directories to backup
|
||||
BACKUP_ITEMS=(
|
||||
"src/"
|
||||
"public/"
|
||||
"astro.config.mjs"
|
||||
"tailwind.config.mjs"
|
||||
"playwright.config.ts"
|
||||
"package.json"
|
||||
"package-lock.json"
|
||||
".env"
|
||||
".env.example"
|
||||
"tsconfig.json"
|
||||
"DEPLOYMENT.md"
|
||||
"README.md"
|
||||
".agents/"
|
||||
)
|
||||
|
||||
# Files to exclude
|
||||
EXCLUDE_PATTERNS=(
|
||||
"node_modules"
|
||||
"dist"
|
||||
".astro"
|
||||
"test-results"
|
||||
".git"
|
||||
"backups"
|
||||
"*.log"
|
||||
)
|
||||
|
||||
# Build exclude arguments
|
||||
EXCLUDE_ARGS=""
|
||||
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
|
||||
EXCLUDE_ARGS="$EXCLUDE_ARGS --exclude=$pattern"
|
||||
done
|
||||
|
||||
# Verify critical files exist
|
||||
log "Verifying critical files..."
|
||||
CRITICAL_FILES=("src/" "package.json" "astro.config.mjs")
|
||||
for file in "${CRITICAL_FILES[@]}"; do
|
||||
if [ ! -e "$PROJECT_ROOT/$file" ]; then
|
||||
log "${RED}ERROR: Critical file/directory missing: $file${NC}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Create backup
|
||||
log "Creating backup archive: $BACKUP_FILE"
|
||||
cd "$PROJECT_ROOT"
|
||||
tar -czf "$BACKUP_FILE" $EXCLUDE_ARGS "${BACKUP_ITEMS[@]}" 2>/dev/null || {
|
||||
log "${RED}ERROR: Backup creation failed${NC}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Verify backup was created
|
||||
if [ ! -f "$BACKUP_FILE" ]; then
|
||||
log "${RED}ERROR: Backup file was not created${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get backup size
|
||||
BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
||||
log "${GREEN}Backup created successfully${NC}"
|
||||
log "Backup file: $BACKUP_FILE"
|
||||
log "Backup size: $BACKUP_SIZE"
|
||||
|
||||
# Verify backup integrity
|
||||
log "Verifying backup integrity..."
|
||||
tar -tzf "$BACKUP_FILE" > /dev/null 2>&1 || {
|
||||
log "${RED}ERROR: Backup verification failed - archive is corrupted${NC}"
|
||||
rm "$BACKUP_FILE"
|
||||
exit 1
|
||||
}
|
||||
|
||||
log "${GREEN}Backup integrity verified${NC}"
|
||||
|
||||
# Count files in backup
|
||||
FILE_COUNT=$(tar -tzf "$BACKUP_FILE" | wc -l)
|
||||
log "Files backed up: $FILE_COUNT"
|
||||
|
||||
# Create latest symlink
|
||||
LATEST_LINK="$BACKUP_DIR/latest-full.tar.gz"
|
||||
ln -sf "$BACKUP_FILE" "$LATEST_LINK"
|
||||
log "Latest backup link created: $LATEST_LINK"
|
||||
|
||||
# Cleanup old backups (keep last 7 days)
|
||||
log "Cleaning up old daily backups (keeping last 7)..."
|
||||
cd "$BACKUP_DIR"
|
||||
ls -t full-backup-*.tar.gz 2>/dev/null | tail -n +8 | xargs -r rm -f
|
||||
log "Cleanup completed"
|
||||
|
||||
# Weekly backup (if Sunday)
|
||||
if [ "$(date +%u)" -eq 7 ]; then
|
||||
WEEKLY_DIR="$PROJECT_ROOT/backups/weekly"
|
||||
mkdir -p "$WEEKLY_DIR"
|
||||
WEEKLY_FILE="$WEEKLY_DIR/weekly-backup-$(date +%Y-%m-%d).tar.gz"
|
||||
cp "$BACKUP_FILE" "$WEEKLY_FILE"
|
||||
log "${GREEN}Weekly backup created: $WEEKLY_FILE${NC}"
|
||||
|
||||
# Cleanup old weekly backups (keep last 4 weeks)
|
||||
cd "$WEEKLY_DIR"
|
||||
ls -t weekly-backup-*.tar.gz 2>/dev/null | tail -n +5 | xargs -r rm -f
|
||||
fi
|
||||
|
||||
# Monthly backup (if 1st of month)
|
||||
if [ "$(date +%d)" -eq 1 ]; then
|
||||
MONTHLY_DIR="$PROJECT_ROOT/backups/monthly"
|
||||
mkdir -p "$MONTHLY_DIR"
|
||||
MONTHLY_FILE="$MONTHLY_DIR/monthly-backup-$(date +%Y-%m).tar.gz"
|
||||
cp "$BACKUP_FILE" "$MONTHLY_FILE"
|
||||
log "${GREEN}Monthly backup created: $MONTHLY_FILE${NC}"
|
||||
|
||||
# Cleanup old monthly backups (keep last 3 months)
|
||||
cd "$MONTHLY_DIR"
|
||||
ls -t monthly-backup-*.tar.gz 2>/dev/null | tail -n +4 | xargs -r rm -f
|
||||
fi
|
||||
|
||||
log "${GREEN}Full backup completed successfully${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}✓ Backup completed${NC}"
|
||||
echo -e " Location: $BACKUP_FILE"
|
||||
echo -e " Size: $BACKUP_SIZE"
|
||||
echo -e " Files: $FILE_COUNT"
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/bin/bash
|
||||
# Cleanup Old Backups Script
|
||||
# Enforces retention policy across all backup types
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
BACKUP_ROOT="$PROJECT_ROOT/backups"
|
||||
LOG_FILE="$BACKUP_ROOT/cleanup.log"
|
||||
|
||||
# Retention periods (in days)
|
||||
DAILY_RETENTION=7
|
||||
INCREMENTAL_RETENTION=3
|
||||
SAFETY_RETENTION=30
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo "[$(date +"%Y-%m-%d %H:%M:%S")] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
log "${YELLOW}Starting backup cleanup...${NC}"
|
||||
|
||||
# Daily backups - keep last 7
|
||||
if [ -d "$BACKUP_ROOT/daily" ]; then
|
||||
log "Cleaning daily backups (keeping last $DAILY_RETENTION)..."
|
||||
cd "$BACKUP_ROOT/daily"
|
||||
DELETED=$(ls -t full-backup-*.tar.gz 2>/dev/null | tail -n +$((DAILY_RETENTION + 1)) | wc -l)
|
||||
ls -t full-backup-*.tar.gz 2>/dev/null | tail -n +$((DAILY_RETENTION + 1)) | xargs -r rm -f
|
||||
log " Deleted $DELETED daily backups"
|
||||
fi
|
||||
|
||||
# Weekly backups - keep last 4
|
||||
if [ -d "$BACKUP_ROOT/weekly" ]; then
|
||||
log "Cleaning weekly backups (keeping last 4)..."
|
||||
cd "$BACKUP_ROOT/weekly"
|
||||
DELETED=$(ls -t weekly-backup-*.tar.gz 2>/dev/null | tail -n +5 | wc -l)
|
||||
ls -t weekly-backup-*.tar.gz 2>/dev/null | tail -n +5 | xargs -r rm -f
|
||||
log " Deleted $DELETED weekly backups"
|
||||
fi
|
||||
|
||||
# Monthly backups - keep last 3
|
||||
if [ -d "$BACKUP_ROOT/monthly" ]; then
|
||||
log "Cleaning monthly backups (keeping last 3)..."
|
||||
cd "$BACKUP_ROOT/monthly"
|
||||
DELETED=$(ls -t monthly-backup-*.tar.gz 2>/dev/null | tail -n +4 | wc -l)
|
||||
ls -t monthly-backup-*.tar.gz 2>/dev/null | tail -n +4 | xargs -r rm -f
|
||||
log " Deleted $DELETED monthly backups"
|
||||
fi
|
||||
|
||||
# Incremental backups - keep last 3 days
|
||||
if [ -d "$BACKUP_ROOT/incremental" ]; then
|
||||
log "Cleaning incremental backups (keeping last $INCREMENTAL_RETENTION days)..."
|
||||
cd "$BACKUP_ROOT/incremental"
|
||||
DELETED=$(find . -name "content-*.tar.gz" -type f -mtime +$INCREMENTAL_RETENTION 2>/dev/null | wc -l)
|
||||
find . -name "content-*.tar.gz" -type f -mtime +$INCREMENTAL_RETENTION -delete 2>/dev/null || true
|
||||
log " Deleted $DELETED incremental backups"
|
||||
fi
|
||||
|
||||
# Pre-deploy config backups - keep last 10
|
||||
if [ -d "$BACKUP_ROOT/pre-deploy" ]; then
|
||||
log "Cleaning pre-deploy backups (keeping last 10)..."
|
||||
cd "$BACKUP_ROOT/pre-deploy"
|
||||
DELETED=$(ls -t config-*.tar.gz 2>/dev/null | tail -n +11 | wc -l)
|
||||
ls -t config-*.tar.gz 2>/dev/null | tail -n +11 | xargs -r rm -f
|
||||
log " Deleted $DELETED pre-deploy backups"
|
||||
fi
|
||||
|
||||
# Safety backups - keep last 30 days
|
||||
if [ -d "$BACKUP_ROOT/safety" ]; then
|
||||
log "Cleaning safety backups (keeping last $SAFETY_RETENTION days)..."
|
||||
cd "$BACKUP_ROOT/safety"
|
||||
DELETED=$(find . -name "pre-restore-*.tar.gz" -type f -mtime +$SAFETY_RETENTION 2>/dev/null | wc -l)
|
||||
find . -name "pre-restore-*.tar.gz" -type f -mtime +$SAFETY_RETENTION -delete 2>/dev/null || true
|
||||
log " Deleted $DELETED safety backups"
|
||||
fi
|
||||
|
||||
# Report storage usage
|
||||
log "Current backup storage usage:"
|
||||
du -sh "$BACKUP_ROOT"/* 2>/dev/null | tee -a "$LOG_FILE" || true
|
||||
|
||||
# Check for large backups (>500MB)
|
||||
log "Checking for large backups (>500MB)..."
|
||||
find "$BACKUP_ROOT" -name "*.tar.gz" -type f -size +500M -exec ls -lh {} \; 2>/dev/null | tee -a "$LOG_FILE" || true
|
||||
|
||||
log "${GREEN}Cleanup completed${NC}"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo -e "${GREEN}=== Cleanup Summary ===${NC}"
|
||||
echo -e "Total backup storage: $(du -sh $BACKUP_ROOT | cut -f1)"
|
||||
echo -e "Daily backups: $(ls $BACKUP_ROOT/daily/*.tar.gz 2>/dev/null | wc -l)"
|
||||
echo -e "Weekly backups: $(ls $BACKUP_ROOT/weekly/*.tar.gz 2>/dev/null | wc -l)"
|
||||
echo -e "Monthly backups: $(ls $BACKUP_ROOT/monthly/*.tar.gz 2>/dev/null | wc -l)"
|
||||
echo -e "Pre-deploy backups: $(ls $BACKUP_ROOT/pre-deploy/*.tar.gz 2>/dev/null | wc -l)"
|
||||
@@ -0,0 +1,54 @@
|
||||
# WorkRoot Backup Automation - Cron Configuration
|
||||
#
|
||||
# To install:
|
||||
# 1. Edit this file and replace /path/to/project with your actual project path
|
||||
# 2. Add to your crontab: crontab -e
|
||||
# 3. Paste the lines below (excluding comments)
|
||||
# 4. Save and exit
|
||||
#
|
||||
# Verify cron jobs: crontab -l
|
||||
|
||||
# ============================================
|
||||
# Daily Full Backup - 2:00 AM every day
|
||||
# ============================================
|
||||
0 2 * * * cd /path/to/project && /usr/bin/npm run backup:full >> /path/to/project/backups/cron.log 2>&1
|
||||
|
||||
# ============================================
|
||||
# Incremental Content Backup - Every 6 hours
|
||||
# ============================================
|
||||
0 */6 * * * cd /path/to/project && /usr/bin/npm run backup:content >> /path/to/project/backups/cron.log 2>&1
|
||||
|
||||
# ============================================
|
||||
# Cleanup Old Backups - Sunday at 3:00 AM
|
||||
# ============================================
|
||||
0 3 * * 0 cd /path/to/project && /usr/bin/npm run backup:cleanup >> /path/to/project/backups/cron.log 2>&1
|
||||
|
||||
# ============================================
|
||||
# Verify Backup Health - Daily at 9:00 AM
|
||||
# ============================================
|
||||
0 9 * * * cd /path/to/project && /usr/bin/npm run backup:verify >> /path/to/project/backups/verify.log 2>&1
|
||||
|
||||
# ============================================
|
||||
# Optional: Monthly backup verification drill
|
||||
# First day of each month at 10:00 AM
|
||||
# ============================================
|
||||
# 0 10 1 * * cd /path/to/project && /path/to/project/scripts/backup/test-restore.sh >> /path/to/project/backups/drill.log 2>&1
|
||||
|
||||
|
||||
# ============================================
|
||||
# Cron Syntax Reference
|
||||
# ============================================
|
||||
# * * * * * command
|
||||
# │ │ │ │ │
|
||||
# │ │ │ │ └─── Day of week (0-7, Sunday = 0 or 7)
|
||||
# │ │ │ └───── Month (1-12)
|
||||
# │ │ └─────── Day of month (1-31)
|
||||
# │ └───────── Hour (0-23)
|
||||
# └─────────── Minute (0-59)
|
||||
#
|
||||
# Examples:
|
||||
# 0 2 * * * - Daily at 2:00 AM
|
||||
# */15 * * * * - Every 15 minutes
|
||||
# 0 */6 * * * - Every 6 hours
|
||||
# 0 3 * * 0 - Sunday at 3:00 AM
|
||||
# 0 0 1 * * - First day of month at midnight
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/bin/bash
|
||||
# Restore Script for WorkRoot Website
|
||||
# Restores from backup with safety checks
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
LOG_FILE="$PROJECT_ROOT/backups/restore.log"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo "[$(date +"%Y-%m-%d %H:%M:%S")] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Function to list available backups
|
||||
list_backups() {
|
||||
echo -e "${BLUE}Available Backups:${NC}\n"
|
||||
|
||||
echo -e "${YELLOW}Full Backups (Daily):${NC}"
|
||||
ls -lht "$PROJECT_ROOT/backups/daily/"*.tar.gz 2>/dev/null | head -n 10 || echo " No daily backups found"
|
||||
|
||||
echo -e "\n${YELLOW}Weekly Backups:${NC}"
|
||||
ls -lht "$PROJECT_ROOT/backups/weekly/"*.tar.gz 2>/dev/null | head -n 5 || echo " No weekly backups found"
|
||||
|
||||
echo -e "\n${YELLOW}Monthly Backups:${NC}"
|
||||
ls -lht "$PROJECT_ROOT/backups/monthly/"*.tar.gz 2>/dev/null | head -n 3 || echo " No monthly backups found"
|
||||
|
||||
echo -e "\n${YELLOW}Config Backups (Pre-deploy):${NC}"
|
||||
ls -lht "$PROJECT_ROOT/backups/pre-deploy/"*.tar.gz 2>/dev/null | head -n 5 || echo " No config backups found"
|
||||
}
|
||||
|
||||
# Function to verify backup integrity
|
||||
verify_backup() {
|
||||
local backup_file="$1"
|
||||
|
||||
if [ ! -f "$backup_file" ]; then
|
||||
echo -e "${RED}ERROR: Backup file not found: $backup_file${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Verifying backup integrity...${NC}"
|
||||
if tar -tzf "$backup_file" > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓ Backup integrity verified${NC}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗ Backup is corrupted${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to create safety backup before restore
|
||||
safety_backup() {
|
||||
echo -e "${YELLOW}Creating safety backup of current state...${NC}"
|
||||
SAFETY_DIR="$PROJECT_ROOT/backups/safety"
|
||||
mkdir -p "$SAFETY_DIR"
|
||||
SAFETY_FILE="$SAFETY_DIR/pre-restore-$(date +%Y-%m-%d_%H-%M-%S).tar.gz"
|
||||
|
||||
tar -czf "$SAFETY_FILE" \
|
||||
--exclude=node_modules \
|
||||
--exclude=dist \
|
||||
--exclude=.astro \
|
||||
--exclude=backups \
|
||||
src/ public/ *.config.* package.json .env 2>/dev/null || true
|
||||
|
||||
echo -e "${GREEN}✓ Safety backup created: $SAFETY_FILE${NC}"
|
||||
}
|
||||
|
||||
# Main restore function
|
||||
restore_backup() {
|
||||
local backup_file="$1"
|
||||
local restore_path="${2:-$PROJECT_ROOT}"
|
||||
|
||||
log "${BLUE}Starting restore process...${NC}"
|
||||
log "Backup file: $backup_file"
|
||||
log "Restore path: $restore_path"
|
||||
|
||||
# Verify backup
|
||||
verify_backup "$backup_file" || exit 1
|
||||
|
||||
# Show backup contents
|
||||
echo -e "\n${YELLOW}Backup contains:${NC}"
|
||||
tar -tzf "$backup_file" | head -n 20
|
||||
echo "..."
|
||||
|
||||
# Confirm restore
|
||||
echo -e "\n${RED}WARNING: This will overwrite current files!${NC}"
|
||||
read -p "Continue with restore? (yes/no): " confirm
|
||||
if [ "$confirm" != "yes" ]; then
|
||||
echo "Restore cancelled"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create safety backup
|
||||
safety_backup
|
||||
|
||||
# Extract backup
|
||||
echo -e "\n${YELLOW}Extracting backup...${NC}"
|
||||
cd "$restore_path"
|
||||
tar -xzf "$backup_file" || {
|
||||
echo -e "${RED}ERROR: Restore failed${NC}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo -e "${GREEN}✓ Files extracted successfully${NC}"
|
||||
|
||||
# Verify critical files
|
||||
echo -e "\n${YELLOW}Verifying restored files...${NC}"
|
||||
CRITICAL_FILES=("src/" "package.json" "astro.config.mjs")
|
||||
for file in "${CRITICAL_FILES[@]}"; do
|
||||
if [ -e "$file" ]; then
|
||||
echo -e " ${GREEN}✓${NC} $file"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} $file (missing)"
|
||||
fi
|
||||
done
|
||||
|
||||
# Post-restore instructions
|
||||
echo -e "\n${GREEN}=== Restore Completed ===${NC}"
|
||||
echo -e "\n${YELLOW}Next steps:${NC}"
|
||||
echo "1. Verify .env file and update secrets if needed"
|
||||
echo "2. Run: npm install"
|
||||
echo "3. Run: npm run build"
|
||||
echo "4. Run: npm run test"
|
||||
echo "5. Check logs for any issues"
|
||||
|
||||
log "${GREEN}Restore completed successfully${NC}"
|
||||
}
|
||||
|
||||
# Main script logic
|
||||
case "${1:-}" in
|
||||
list|--list|-l)
|
||||
list_backups
|
||||
;;
|
||||
verify|--verify|-v)
|
||||
if [ -z "$2" ]; then
|
||||
echo "Usage: $0 verify <backup-file>"
|
||||
exit 1
|
||||
fi
|
||||
verify_backup "$2"
|
||||
;;
|
||||
*)
|
||||
if [ -z "$1" ]; then
|
||||
echo -e "${YELLOW}Usage:${NC}"
|
||||
echo " $0 list - List available backups"
|
||||
echo " $0 verify <backup-file> - Verify backup integrity"
|
||||
echo " $0 <backup-file> - Restore from backup"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Quick restore from latest:${NC}"
|
||||
echo " $0 backups/daily/latest-full.tar.gz"
|
||||
exit 1
|
||||
fi
|
||||
restore_backup "$1" "$2"
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
# Backup System Setup Script
|
||||
# One-time setup for the automated backup system
|
||||
|
||||
set -e
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${BLUE}=== WorkRoot Backup System Setup ===${NC}\n"
|
||||
|
||||
# Step 1: Create directory structure
|
||||
echo -e "${YELLOW}Step 1: Creating backup directories...${NC}"
|
||||
mkdir -p "$PROJECT_ROOT/backups"/{daily,weekly,monthly,pre-deploy,incremental,safety}
|
||||
echo -e "${GREEN}✓ Directories created${NC}\n"
|
||||
|
||||
# Step 2: Make scripts executable
|
||||
echo -e "${YELLOW}Step 2: Making scripts executable...${NC}"
|
||||
chmod +x "$PROJECT_ROOT/scripts/backup"/*.sh
|
||||
echo -e "${GREEN}✓ Scripts are now executable${NC}\n"
|
||||
|
||||
# Step 3: Test backup
|
||||
echo -e "${YELLOW}Step 3: Running test backup...${NC}"
|
||||
cd "$PROJECT_ROOT"
|
||||
bash scripts/backup/backup-config.sh
|
||||
echo -e "${GREEN}✓ Test backup completed${NC}\n"
|
||||
|
||||
# Step 4: Verify backup
|
||||
echo -e "${YELLOW}Step 4: Verifying backup system...${NC}"
|
||||
bash scripts/backup/verify.sh
|
||||
echo ""
|
||||
|
||||
# Step 5: Instructions
|
||||
echo -e "${BLUE}=== Setup Complete! ===${NC}\n"
|
||||
|
||||
echo -e "${GREEN}Next Steps:${NC}"
|
||||
echo "1. Test full backup:"
|
||||
echo -e " ${YELLOW}npm run backup:full${NC}\n"
|
||||
|
||||
echo "2. Set up automation (choose one):"
|
||||
echo -e " ${YELLOW}Linux/macOS:${NC} Edit cron jobs"
|
||||
echo " crontab -e"
|
||||
echo " (See scripts/backup/cron.example for template)"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}Windows:${NC} Run PowerShell as Administrator"
|
||||
echo " .\\scripts\\backup\\windows-tasks.ps1"
|
||||
echo ""
|
||||
|
||||
echo "3. Verify automated backups work:"
|
||||
echo -e " ${YELLOW}npm run backup:verify${NC}\n"
|
||||
|
||||
echo -e "${GREEN}Available Commands:${NC}"
|
||||
echo " npm run backup:full - Full backup"
|
||||
echo " npm run backup:content - Content only"
|
||||
echo " npm run backup:config - Config files only"
|
||||
echo " npm run backup:verify - Check health"
|
||||
echo " npm run backup:list - List backups"
|
||||
echo " npm run backup:restore - Restore from backup"
|
||||
echo " npm run backup:cleanup - Remove old backups"
|
||||
echo ""
|
||||
|
||||
echo -e "${BLUE}Documentation:${NC}"
|
||||
echo " scripts/backup/README.md"
|
||||
echo " .agents/devops-engineer/BACKUP_STRATEGY.md"
|
||||
echo ""
|
||||
|
||||
echo -e "${GREEN}Setup completed successfully!${NC}"
|
||||
@@ -0,0 +1,177 @@
|
||||
#!/bin/bash
|
||||
# Backup Verification Script
|
||||
# Checks backup health, integrity, and provides status report
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
BACKUP_ROOT="$PROJECT_ROOT/backups"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Counters
|
||||
TOTAL_BACKUPS=0
|
||||
VALID_BACKUPS=0
|
||||
CORRUPTED_BACKUPS=0
|
||||
WARNINGS=0
|
||||
|
||||
echo -e "${BLUE}=== Backup Verification Report ===${NC}\n"
|
||||
echo "Generated: $(date)"
|
||||
echo "Backup location: $BACKUP_ROOT"
|
||||
echo ""
|
||||
|
||||
# Function to verify a single backup
|
||||
verify_single_backup() {
|
||||
local backup_file="$1"
|
||||
TOTAL_BACKUPS=$((TOTAL_BACKUPS + 1))
|
||||
|
||||
if tar -tzf "$backup_file" > /dev/null 2>&1; then
|
||||
VALID_BACKUPS=$((VALID_BACKUPS + 1))
|
||||
return 0
|
||||
else
|
||||
CORRUPTED_BACKUPS=$((CORRUPTED_BACKUPS + 1))
|
||||
echo -e " ${RED}✗ CORRUPTED: $backup_file${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check daily backups
|
||||
echo -e "${YELLOW}Daily Backups:${NC}"
|
||||
if [ -d "$BACKUP_ROOT/daily" ]; then
|
||||
DAILY_COUNT=$(ls "$BACKUP_ROOT/daily"/*.tar.gz 2>/dev/null | wc -l)
|
||||
echo " Found: $DAILY_COUNT backups"
|
||||
|
||||
if [ "$DAILY_COUNT" -eq 0 ]; then
|
||||
echo -e " ${RED}⚠ WARNING: No daily backups found${NC}"
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
else
|
||||
# Check latest backup age
|
||||
LATEST=$(ls -t "$BACKUP_ROOT/daily"/*.tar.gz 2>/dev/null | head -n 1)
|
||||
AGE_HOURS=$(( ($(date +%s) - $(stat -c %Y "$LATEST" 2>/dev/null || stat -f %m "$LATEST")) / 3600 ))
|
||||
echo " Latest backup: $(basename "$LATEST")"
|
||||
echo " Age: ${AGE_HOURS} hours ago"
|
||||
|
||||
if [ "$AGE_HOURS" -gt 24 ]; then
|
||||
echo -e " ${RED}⚠ WARNING: Latest backup is >24 hours old${NC}"
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
fi
|
||||
|
||||
# Verify integrity
|
||||
for backup in "$BACKUP_ROOT/daily"/*.tar.gz; do
|
||||
verify_single_backup "$backup" > /dev/null
|
||||
done
|
||||
echo -e " ${GREEN}✓ All daily backups verified${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e " ${RED}✗ Daily backup directory not found${NC}"
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check weekly backups
|
||||
echo -e "${YELLOW}Weekly Backups:${NC}"
|
||||
if [ -d "$BACKUP_ROOT/weekly" ]; then
|
||||
WEEKLY_COUNT=$(ls "$BACKUP_ROOT/weekly"/*.tar.gz 2>/dev/null | wc -l)
|
||||
echo " Found: $WEEKLY_COUNT backups"
|
||||
|
||||
if ls "$BACKUP_ROOT/weekly"/*.tar.gz 1> /dev/null 2>&1; then
|
||||
for backup in "$BACKUP_ROOT/weekly"/*.tar.gz; do
|
||||
verify_single_backup "$backup" > /dev/null
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$WEEKLY_COUNT" -gt 0 ]; then
|
||||
echo -e " ${GREEN}✓ All weekly backups verified${NC}"
|
||||
fi
|
||||
else
|
||||
echo " No weekly backups directory"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check monthly backups
|
||||
echo -e "${YELLOW}Monthly Backups:${NC}"
|
||||
if [ -d "$BACKUP_ROOT/monthly" ]; then
|
||||
MONTHLY_COUNT=$(ls "$BACKUP_ROOT/monthly"/*.tar.gz 2>/dev/null | wc -l)
|
||||
echo " Found: $MONTHLY_COUNT backups"
|
||||
|
||||
if ls "$BACKUP_ROOT/monthly"/*.tar.gz 1> /dev/null 2>&1; then
|
||||
for backup in "$BACKUP_ROOT/monthly"/*.tar.gz; do
|
||||
verify_single_backup "$backup" > /dev/null
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$MONTHLY_COUNT" -gt 0 ]; then
|
||||
echo -e " ${GREEN}✓ All monthly backups verified${NC}"
|
||||
fi
|
||||
else
|
||||
echo " No monthly backups directory"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Storage usage
|
||||
echo -e "${YELLOW}Storage Usage:${NC}"
|
||||
if [ -d "$BACKUP_ROOT" ]; then
|
||||
TOTAL_SIZE=$(du -sh "$BACKUP_ROOT" 2>/dev/null | cut -f1)
|
||||
echo " Total backup size: $TOTAL_SIZE"
|
||||
|
||||
# Check if approaching limits (>5GB)
|
||||
SIZE_MB=$(du -sm "$BACKUP_ROOT" 2>/dev/null | cut -f1)
|
||||
if [ "$SIZE_MB" -gt 5120 ]; then
|
||||
echo -e " ${YELLOW}⚠ WARNING: Backup storage >5GB, consider cleanup${NC}"
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check disk space
|
||||
echo -e "${YELLOW}Disk Space:${NC}"
|
||||
DISK_USAGE=$(df -h "$BACKUP_ROOT" | tail -1)
|
||||
DISK_PERCENT=$(echo "$DISK_USAGE" | awk '{print $5}' | sed 's/%//')
|
||||
echo " Disk usage: $DISK_PERCENT%"
|
||||
|
||||
if [ "$DISK_PERCENT" -gt 80 ]; then
|
||||
echo -e " ${RED}⚠ WARNING: Disk usage >80%${NC}"
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Summary
|
||||
echo -e "${BLUE}=== Summary ===${NC}"
|
||||
echo "Total backups checked: $TOTAL_BACKUPS"
|
||||
echo -e "Valid backups: ${GREEN}$VALID_BACKUPS${NC}"
|
||||
|
||||
if [ "$CORRUPTED_BACKUPS" -gt 0 ]; then
|
||||
echo -e "Corrupted backups: ${RED}$CORRUPTED_BACKUPS${NC}"
|
||||
fi
|
||||
|
||||
if [ "$WARNINGS" -gt 0 ]; then
|
||||
echo -e "Warnings: ${YELLOW}$WARNINGS${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Final status
|
||||
if [ "$CORRUPTED_BACKUPS" -eq 0 ] && [ "$WARNINGS" -eq 0 ] && [ "$VALID_BACKUPS" -gt 0 ]; then
|
||||
echo -e "${GREEN}✓ Backup system healthy${NC}"
|
||||
exit 0
|
||||
elif [ "$CORRUPTED_BACKUPS" -gt 0 ]; then
|
||||
echo -e "${RED}✗ CRITICAL: Corrupted backups detected${NC}"
|
||||
exit 2
|
||||
elif [ "$WARNINGS" -gt 0 ]; then
|
||||
echo -e "${YELLOW}⚠ Backup system has warnings${NC}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${YELLOW}⚠ No backups found - run initial backup${NC}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,67 @@
|
||||
# WorkRoot Backup Automation - Windows Task Scheduler Setup
|
||||
#
|
||||
# Run this PowerShell script as Administrator to set up automated backups on Windows
|
||||
#
|
||||
# Usage: .\windows-tasks.ps1
|
||||
|
||||
# Get the project root directory
|
||||
$ProjectRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
|
||||
$LogPath = Join-Path $ProjectRoot "backups"
|
||||
|
||||
Write-Host "Setting up Windows Task Scheduler for backups..." -ForegroundColor Cyan
|
||||
Write-Host "Project root: $ProjectRoot" -ForegroundColor Yellow
|
||||
|
||||
# Ensure log directory exists
|
||||
if (-not (Test-Path $LogPath)) {
|
||||
New-Item -ItemType Directory -Path $LogPath -Force | Out-Null
|
||||
}
|
||||
|
||||
# Daily Full Backup at 2:00 AM
|
||||
Write-Host "`nCreating daily backup task..." -ForegroundColor Green
|
||||
$DailyAction = New-ScheduledTaskAction -Execute "npm" -Argument "run backup:full" -WorkingDirectory $ProjectRoot
|
||||
$DailyTrigger = New-ScheduledTaskTrigger -Daily -At 2am
|
||||
$DailySettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||
Register-ScheduledTask -TaskName "WorkRoot-DailyBackup" -Action $DailyAction -Trigger $DailyTrigger -Settings $DailySettings -Description "WorkRoot daily full backup" -Force | Out-Null
|
||||
Write-Host "✓ Daily backup scheduled for 2:00 AM" -ForegroundColor Green
|
||||
|
||||
# Incremental Backup Every 6 Hours
|
||||
Write-Host "`nCreating incremental backup task..." -ForegroundColor Green
|
||||
$IncrementalAction = New-ScheduledTaskAction -Execute "npm" -Argument "run backup:content" -WorkingDirectory $ProjectRoot
|
||||
$IncrementalTrigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Hours 6) -RepetitionDuration ([TimeSpan]::MaxValue)
|
||||
Register-ScheduledTask -TaskName "WorkRoot-IncrementalBackup" -Action $IncrementalAction -Trigger $IncrementalTrigger -Settings $DailySettings -Description "WorkRoot incremental content backup" -Force | Out-Null
|
||||
Write-Host "✓ Incremental backup scheduled every 6 hours" -ForegroundColor Green
|
||||
|
||||
# Weekly Cleanup on Sunday at 3:00 AM
|
||||
Write-Host "`nCreating cleanup task..." -ForegroundColor Green
|
||||
$CleanupAction = New-ScheduledTaskAction -Execute "npm" -Argument "run backup:cleanup" -WorkingDirectory $ProjectRoot
|
||||
$CleanupTrigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 3am
|
||||
Register-ScheduledTask -TaskName "WorkRoot-BackupCleanup" -Action $CleanupAction -Trigger $CleanupTrigger -Settings $DailySettings -Description "WorkRoot backup cleanup" -Force | Out-Null
|
||||
Write-Host "✓ Cleanup scheduled for Sunday 3:00 AM" -ForegroundColor Green
|
||||
|
||||
# Daily Verification at 9:00 AM
|
||||
Write-Host "`nCreating verification task..." -ForegroundColor Green
|
||||
$VerifyAction = New-ScheduledTaskAction -Execute "npm" -Argument "run backup:verify" -WorkingDirectory $ProjectRoot
|
||||
$VerifyTrigger = New-ScheduledTaskTrigger -Daily -At 9am
|
||||
Register-ScheduledTask -TaskName "WorkRoot-BackupVerify" -Action $VerifyAction -Trigger $VerifyTrigger -Settings $DailySettings -Description "WorkRoot backup verification" -Force | Out-Null
|
||||
Write-Host "✓ Verification scheduled for 9:00 AM daily" -ForegroundColor Green
|
||||
|
||||
Write-Host "`n========================================" -ForegroundColor Cyan
|
||||
Write-Host "Backup automation setup complete!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nScheduled Tasks Created:" -ForegroundColor Yellow
|
||||
Write-Host " • Daily Full Backup - Every day at 2:00 AM"
|
||||
Write-Host " • Incremental Backup - Every 6 hours"
|
||||
Write-Host " • Weekly Cleanup - Sunday at 3:00 AM"
|
||||
Write-Host " • Daily Verification - Every day at 9:00 AM"
|
||||
|
||||
Write-Host "`nTo view scheduled tasks:" -ForegroundColor Yellow
|
||||
Write-Host " Get-ScheduledTask | Where-Object {`$_.TaskName -like 'WorkRoot-*'}"
|
||||
|
||||
Write-Host "`nTo remove all tasks:" -ForegroundColor Yellow
|
||||
Write-Host " Get-ScheduledTask | Where-Object {`$_.TaskName -like 'WorkRoot-*'} | Unregister-ScheduledTask -Confirm:`$false"
|
||||
|
||||
Write-Host "`nTo run a backup now (test):" -ForegroundColor Yellow
|
||||
Write-Host " npm run backup:full"
|
||||
|
||||
Write-Host "`n" -ForegroundColor Green
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Lighthouse Performance Audit Script
|
||||
Runs Lighthouse audits on specified URLs and reports results
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
def run_lighthouse(url, output_file=None):
|
||||
"""Run Lighthouse audit on a URL"""
|
||||
cmd = [
|
||||
'npx', 'lighthouse', url,
|
||||
'--output=json',
|
||||
'--output-path=stdout',
|
||||
'--chrome-flags="--headless --no-sandbox"',
|
||||
'--quiet'
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
||||
if result.returncode != 0:
|
||||
print(f"Error running Lighthouse on {url}: {result.stderr}")
|
||||
return None
|
||||
|
||||
data = json.loads(result.stdout)
|
||||
return data
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f"Timeout running Lighthouse on {url}")
|
||||
return None
|
||||
except json.JSONDecodeError:
|
||||
print(f"Failed to parse Lighthouse output for {url}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Unexpected error for {url}: {e}")
|
||||
return None
|
||||
|
||||
def extract_scores(lighthouse_data):
|
||||
"""Extract key scores from Lighthouse data"""
|
||||
if not lighthouse_data or 'categories' not in lighthouse_data:
|
||||
return None
|
||||
|
||||
categories = lighthouse_data['categories']
|
||||
return {
|
||||
'performance': int(categories.get('performance', {}).get('score', 0) * 100),
|
||||
'accessibility': int(categories.get('accessibility', {}).get('score', 0) * 100),
|
||||
'best-practices': int(categories.get('best-practices', {}).get('score', 0) * 100),
|
||||
'seo': int(categories.get('seo', {}).get('score', 0) * 100),
|
||||
}
|
||||
|
||||
def get_issues(lighthouse_data):
|
||||
"""Extract issues from Lighthouse audit"""
|
||||
if not lighthouse_data or 'audits' not in lighthouse_data:
|
||||
return []
|
||||
|
||||
issues = []
|
||||
audits = lighthouse_data['audits']
|
||||
|
||||
# Key audits to check
|
||||
important_audits = [
|
||||
'first-contentful-paint',
|
||||
'largest-contentful-paint',
|
||||
'cumulative-layout-shift',
|
||||
'total-blocking-time',
|
||||
'speed-index',
|
||||
'interactive',
|
||||
'image-alt',
|
||||
'document-title',
|
||||
'meta-description',
|
||||
'link-text',
|
||||
'crawlable-anchors',
|
||||
'color-contrast',
|
||||
'tap-targets',
|
||||
'viewport',
|
||||
'uses-responsive-images',
|
||||
'modern-image-formats',
|
||||
'offscreen-images',
|
||||
'render-blocking-resources',
|
||||
'unminified-css',
|
||||
'unminified-javascript',
|
||||
'unused-css-rules',
|
||||
'unused-javascript',
|
||||
]
|
||||
|
||||
for audit_id in important_audits:
|
||||
if audit_id in audits:
|
||||
audit = audits[audit_id]
|
||||
if audit.get('score') is not None and audit['score'] < 0.9:
|
||||
issues.append({
|
||||
'id': audit_id,
|
||||
'title': audit.get('title', audit_id),
|
||||
'score': audit.get('score'),
|
||||
'displayValue': audit.get('displayValue', ''),
|
||||
'description': audit.get('description', ''),
|
||||
})
|
||||
|
||||
return issues
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python lighthouse_audit.py <url>")
|
||||
sys.exit(1)
|
||||
|
||||
url = sys.argv[1]
|
||||
print(f"\n🔍 Running Lighthouse audit on: {url}\n")
|
||||
|
||||
data = run_lighthouse(url)
|
||||
if not data:
|
||||
print("❌ Failed to run Lighthouse audit")
|
||||
sys.exit(1)
|
||||
|
||||
scores = extract_scores(data)
|
||||
if not scores:
|
||||
print("❌ Failed to extract scores")
|
||||
sys.exit(1)
|
||||
|
||||
# Print results
|
||||
print("=" * 60)
|
||||
print("LIGHTHOUSE SCORES")
|
||||
print("=" * 60)
|
||||
|
||||
for category, score in scores.items():
|
||||
emoji = "✅" if score >= 90 else "⚠️" if score >= 50 else "❌"
|
||||
print(f"{emoji} {category.upper():20s}: {score:3d}/100")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("ISSUES FOUND")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
issues = get_issues(data)
|
||||
if issues:
|
||||
for issue in issues[:10]: # Show top 10 issues
|
||||
score_str = f"{int(issue['score'] * 100)}/100" if issue['score'] is not None else "N/A"
|
||||
print(f"⚠️ {issue['title']}")
|
||||
print(f" Score: {score_str}")
|
||||
if issue['displayValue']:
|
||||
print(f" Value: {issue['displayValue']}")
|
||||
print()
|
||||
else:
|
||||
print("✅ No major issues found!\n")
|
||||
|
||||
# Check if all scores >= 90
|
||||
all_passed = all(score >= 90 for score in scores.values())
|
||||
if all_passed:
|
||||
print("🎉 All scores are 90+! Great job!\n")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("⚠️ Some scores are below 90. Review issues above.\n")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Performance Test Script
|
||||
Tests SSR response times and API endpoint performance
|
||||
"""
|
||||
|
||||
import time
|
||||
import requests
|
||||
import statistics
|
||||
import json
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
# Test configuration
|
||||
BASE_URL = "http://localhost:10000"
|
||||
NUM_ITERATIONS = 10
|
||||
TIMEOUT = 10
|
||||
|
||||
def measure_response_time(url: str, iterations: int = NUM_ITERATIONS) -> Tuple[List[float], bool]:
|
||||
"""Measure response time for a URL over multiple iterations"""
|
||||
times = []
|
||||
success = True
|
||||
|
||||
for i in range(iterations):
|
||||
try:
|
||||
start = time.time()
|
||||
response = requests.get(url, timeout=TIMEOUT)
|
||||
end = time.time()
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f" Warning: {url} returned status {response.status_code}")
|
||||
success = False
|
||||
|
||||
times.append((end - start) * 1000) # Convert to ms
|
||||
time.sleep(0.1) # Small delay between requests
|
||||
except Exception as e:
|
||||
print(f" Error testing {url}: {e}")
|
||||
success = False
|
||||
|
||||
return times, success
|
||||
|
||||
def print_stats(name: str, times: List[float]):
|
||||
"""Print statistics for response times"""
|
||||
if not times:
|
||||
print(f"\n{name}: No data")
|
||||
return
|
||||
|
||||
avg = statistics.mean(times)
|
||||
median = statistics.median(times)
|
||||
min_time = min(times)
|
||||
max_time = max(times)
|
||||
|
||||
# Performance thresholds
|
||||
status = "OK" if avg < 200 else "WARN" if avg < 500 else "SLOW"
|
||||
|
||||
print(f"\n{name}:")
|
||||
print(f" Average: {avg:.2f}ms [{status}]")
|
||||
print(f" Median: {median:.2f}ms")
|
||||
print(f" Min: {min_time:.2f}ms")
|
||||
print(f" Max: {max_time:.2f}ms")
|
||||
|
||||
if len(times) > 1:
|
||||
stddev = statistics.stdev(times)
|
||||
print(f" Std Dev: {stddev:.2f}ms")
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("PERFORMANCE TEST REPORT")
|
||||
print("=" * 60)
|
||||
print(f"Base URL: {BASE_URL}")
|
||||
print(f"Iterations per endpoint: {NUM_ITERATIONS}")
|
||||
print()
|
||||
|
||||
# Define test endpoints
|
||||
test_pages = {
|
||||
"Homepage": "/",
|
||||
"Case Studies": "/case-studies",
|
||||
"Contact": "/contact",
|
||||
"About": "/about",
|
||||
}
|
||||
|
||||
test_apis = {
|
||||
"Health Check": "/api/health.json",
|
||||
"Sitemap": "/api/sitemap.xml",
|
||||
}
|
||||
|
||||
all_passed = True
|
||||
results = {}
|
||||
|
||||
# Test pages
|
||||
print("\n" + "=" * 60)
|
||||
print("TESTING SSR PAGES")
|
||||
print("=" * 60)
|
||||
|
||||
for name, path in test_pages.items():
|
||||
url = f"{BASE_URL}{path}"
|
||||
print(f"\nTesting {name} ({url})...")
|
||||
times, success = measure_response_time(url)
|
||||
print_stats(name, times)
|
||||
results[name] = times
|
||||
all_passed = all_passed and success
|
||||
|
||||
# Test API endpoints
|
||||
print("\n\n" + "=" * 60)
|
||||
print("TESTING API ENDPOINTS")
|
||||
print("=" * 60)
|
||||
|
||||
for name, path in test_apis.items():
|
||||
url = f"{BASE_URL}{path}"
|
||||
print(f"\nTesting {name} ({url})...")
|
||||
times, success = measure_response_time(url)
|
||||
print_stats(name, times)
|
||||
results[name] = times
|
||||
all_passed = all_passed and success
|
||||
|
||||
# Verify domain in sitemap
|
||||
print("\n\n" + "=" * 60)
|
||||
print("DOMAIN VERIFICATION")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/api/sitemap.xml", timeout=TIMEOUT)
|
||||
if "workroot.in" in response.text:
|
||||
print("\nOK: Sitemap contains 'workroot.in'")
|
||||
else:
|
||||
print("\nWARN: Sitemap does not contain 'workroot.in'")
|
||||
all_passed = False
|
||||
|
||||
if "workroot.com" in response.text:
|
||||
print("ERROR: Sitemap still contains old domain 'workroot.com'")
|
||||
all_passed = False
|
||||
except Exception as e:
|
||||
print(f"\nError checking sitemap: {e}")
|
||||
all_passed = False
|
||||
|
||||
# Overall summary
|
||||
print("\n\n" + "=" * 60)
|
||||
print("PERFORMANCE SUMMARY")
|
||||
print("=" * 60)
|
||||
|
||||
# Calculate average response time across all endpoints
|
||||
all_times = []
|
||||
for times in results.values():
|
||||
all_times.extend(times)
|
||||
|
||||
if all_times:
|
||||
overall_avg = statistics.mean(all_times)
|
||||
print(f"\nOverall Average Response Time: {overall_avg:.2f}ms")
|
||||
|
||||
if overall_avg < 200:
|
||||
print("Performance: EXCELLENT (< 200ms)")
|
||||
elif overall_avg < 500:
|
||||
print("Performance: GOOD (< 500ms)")
|
||||
else:
|
||||
print("Performance: NEEDS IMPROVEMENT (>= 500ms)")
|
||||
|
||||
# Recommendations
|
||||
print("\n" + "=" * 60)
|
||||
print("RECOMMENDATIONS")
|
||||
print("=" * 60)
|
||||
|
||||
slow_endpoints = {name: statistics.mean(times)
|
||||
for name, times in results.items()
|
||||
if times and statistics.mean(times) > 500}
|
||||
|
||||
if slow_endpoints:
|
||||
print("\nSlow endpoints detected (>500ms):")
|
||||
for name, avg_time in slow_endpoints.items():
|
||||
print(f" - {name}: {avg_time:.2f}ms")
|
||||
print("\nConsider:")
|
||||
print(" - Implementing caching")
|
||||
print(" - Optimizing SSR hydration")
|
||||
print(" - Reducing bundle size")
|
||||
else:
|
||||
print("\nAll endpoints performing well! No immediate optimizations needed.")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
if all_passed:
|
||||
print("RESULT: PASS")
|
||||
print("=" * 60)
|
||||
return 0
|
||||
else:
|
||||
print("RESULT: ISSUES DETECTED")
|
||||
print("=" * 60)
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Sitemap Ping Script
|
||||
# Notifies search engines of sitemap updates after deployment
|
||||
# Usage: ./scripts/ping-sitemap.sh
|
||||
|
||||
set -e
|
||||
|
||||
SITE_URL="https://workroot.in"
|
||||
SITEMAP_URL="${SITE_URL}/sitemap.xml"
|
||||
|
||||
echo "🔔 Pinging search engines about sitemap update..."
|
||||
echo "Sitemap: $SITEMAP_URL"
|
||||
echo ""
|
||||
|
||||
# Google
|
||||
echo "📍 Pinging Google..."
|
||||
GOOGLE_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "https://www.google.com/ping?sitemap=${SITEMAP_URL}")
|
||||
if [ "$GOOGLE_RESPONSE" = "200" ]; then
|
||||
echo "✅ Google pinged successfully"
|
||||
else
|
||||
echo "⚠️ Google ping returned HTTP $GOOGLE_RESPONSE"
|
||||
fi
|
||||
|
||||
# Bing
|
||||
echo "📍 Pinging Bing..."
|
||||
BING_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "https://www.bing.com/ping?sitemap=${SITEMAP_URL}")
|
||||
if [ "$BING_RESPONSE" = "200" ]; then
|
||||
echo "✅ Bing pinged successfully"
|
||||
else
|
||||
echo "⚠️ Bing ping returned HTTP $BING_RESPONSE"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✨ Sitemap ping complete!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Check Google Search Console for indexing status"
|
||||
echo "2. Check Bing Webmaster Tools for crawl stats"
|
||||
echo "3. Monitor coverage reports in 24-48 hours"
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
# Quick Performance Check Script
|
||||
# Tests key endpoints and reports response times
|
||||
|
||||
echo "============================================================"
|
||||
echo "QUICK PERFORMANCE CHECK"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
|
||||
BASE_URL="${1:-http://localhost:10000}"
|
||||
echo "Testing: $BASE_URL"
|
||||
echo ""
|
||||
|
||||
# Test endpoints
|
||||
ENDPOINTS=(
|
||||
"/"
|
||||
"/contact"
|
||||
"/about"
|
||||
"/api/health.json"
|
||||
)
|
||||
|
||||
echo "Endpoint Performance:"
|
||||
echo "------------------------------------------------------------"
|
||||
|
||||
for endpoint in "${ENDPOINTS[@]}"; do
|
||||
url="${BASE_URL}${endpoint}"
|
||||
|
||||
# Run curl and capture timing
|
||||
time_total=$(curl -w "%{time_total}" -o /dev/null -s "$url")
|
||||
http_code=$(curl -w "%{http_code}" -o /dev/null -s "$url")
|
||||
|
||||
# Convert to milliseconds
|
||||
time_ms=$(echo "$time_total * 1000" | bc)
|
||||
|
||||
# Determine status
|
||||
if (( $(echo "$time_total < 0.2" | bc -l) )); then
|
||||
status="EXCELLENT"
|
||||
elif (( $(echo "$time_total < 0.5" | bc -l) )); then
|
||||
status="GOOD"
|
||||
else
|
||||
status="SLOW"
|
||||
fi
|
||||
|
||||
printf "%-30s %6.2fms [%s] (HTTP %s)\n" "$endpoint" "$time_ms" "$status" "$http_code"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "Performance Target: < 500ms"
|
||||
echo "Optimal: < 200ms"
|
||||
echo "============================================================"
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# UptimeRobot Monitor Setup Script
|
||||
# Usage: UPTIMEROBOT_API_KEY=your_key ./scripts/setup-uptimerobot.sh
|
||||
#
|
||||
# Creates monitors for:
|
||||
# - Main site health check (every 5 min)
|
||||
# - Homepage
|
||||
# - API health endpoint
|
||||
# - SSL certificate expiry alert
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
API_KEY="${UPTIMEROBOT_API_KEY:-}"
|
||||
ALERT_EMAIL="${ALERT_EMAIL:-}"
|
||||
BASE_URL="https://workroot.in"
|
||||
API_BASE="https://api.uptimerobot.com/v2"
|
||||
|
||||
# ── Validation ────────────────────────────────────────────────────────────────
|
||||
if [ -z "$API_KEY" ]; then
|
||||
echo "ERROR: UPTIMEROBOT_API_KEY environment variable is required"
|
||||
echo "Get your API key from: https://uptimerobot.com/dashboard → My Settings → API Settings"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Helper function ───────────────────────────────────────────────────────────
|
||||
uptimerobot_api() {
|
||||
local endpoint="$1"
|
||||
shift
|
||||
curl -s -X POST "${API_BASE}/${endpoint}" \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
--data "api_key=${API_KEY}&format=json&$*"
|
||||
}
|
||||
|
||||
# ── Get or create alert contact ───────────────────────────────────────────────
|
||||
get_alert_contact_id() {
|
||||
if [ -z "$ALERT_EMAIL" ]; then
|
||||
# Use first existing contact
|
||||
CONTACTS=$(uptimerobot_api "getAlertContacts")
|
||||
CONTACT_ID=$(echo "$CONTACTS" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
contacts = data.get('alert_contacts', [])
|
||||
if contacts:
|
||||
print(contacts[0]['id'])
|
||||
" 2>/dev/null || echo "")
|
||||
echo "$CONTACT_ID"
|
||||
return
|
||||
fi
|
||||
|
||||
# Create new contact for the email
|
||||
RESULT=$(uptimerobot_api "newAlertContact" \
|
||||
"type=2&value=${ALERT_EMAIL}&friendly_name=WorkRoot+Alerts")
|
||||
CONTACT_ID=$(echo "$RESULT" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
print(data.get('alertcontact', {}).get('id', ''))
|
||||
" 2>/dev/null || echo "")
|
||||
echo "$CONTACT_ID"
|
||||
}
|
||||
|
||||
# ── Create monitor ────────────────────────────────────────────────────────────
|
||||
create_monitor() {
|
||||
local name="$1"
|
||||
local url="$2"
|
||||
local type="${3:-1}" # 1=HTTP, 2=keyword, 3=ping
|
||||
local interval="${4:-300}" # seconds
|
||||
local alert_id="$5"
|
||||
local keyword="${6:-}"
|
||||
|
||||
echo "Creating monitor: $name → $url"
|
||||
|
||||
local data="friendly_name=${name}&url=${url}&type=${type}&interval=${interval}"
|
||||
if [ -n "$alert_id" ]; then
|
||||
data="${data}&alert_contacts=${alert_id}_0_0"
|
||||
fi
|
||||
if [ "$type" = "2" ] && [ -n "$keyword" ]; then
|
||||
data="${data}&keyword_type=1&keyword_value=${keyword}"
|
||||
fi
|
||||
|
||||
RESULT=$(uptimerobot_api "newMonitor" "$data")
|
||||
STATUS=$(echo "$RESULT" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
print(data.get('stat', 'fail'))
|
||||
" 2>/dev/null || echo "fail")
|
||||
|
||||
if [ "$STATUS" = "ok" ]; then
|
||||
MONITOR_ID=$(echo "$RESULT" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
print(data.get('monitor', {}).get('id', ''))
|
||||
" 2>/dev/null || echo "")
|
||||
echo " Created monitor ID: $MONITOR_ID"
|
||||
else
|
||||
ERROR=$(echo "$RESULT" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
err = data.get('error', {})
|
||||
print(err.get('message', 'unknown error'))
|
||||
" 2>/dev/null || echo "unknown")
|
||||
echo " WARNING: Failed to create monitor - $ERROR"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
echo "============================================"
|
||||
echo " WorkRoot UptimeRobot Monitor Setup"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
echo "Getting alert contact..."
|
||||
ALERT_ID=$(get_alert_contact_id)
|
||||
if [ -z "$ALERT_ID" ]; then
|
||||
echo "WARNING: No alert contact found/created. Monitors will be created without alerts."
|
||||
echo "Add an alert contact at: https://uptimerobot.com/dashboard → Alert Contacts"
|
||||
fi
|
||||
echo "Alert contact ID: ${ALERT_ID:-none}"
|
||||
echo ""
|
||||
|
||||
# HTTP monitors (every 5 minutes)
|
||||
create_monitor "WorkRoot - Health Check" "${BASE_URL}/api/health.json" "1" "300" "$ALERT_ID"
|
||||
create_monitor "WorkRoot - Homepage" "${BASE_URL}/" "1" "300" "$ALERT_ID"
|
||||
create_monitor "WorkRoot - Services Page" "${BASE_URL}/services" "1" "300" "$ALERT_ID"
|
||||
create_monitor "WorkRoot - Contact Page" "${BASE_URL}/contact" "1" "300" "$ALERT_ID"
|
||||
|
||||
# Keyword monitor - verify health endpoint returns "ok" status
|
||||
create_monitor "WorkRoot - Health Status OK" "${BASE_URL}/api/health.json" "2" "300" "$ALERT_ID" "\"status\":\"ok\""
|
||||
|
||||
# SSL certificate monitor (UptimeRobot checks SSL with HTTP monitors, but
|
||||
# we add explicit keyword check for https redirect)
|
||||
create_monitor "WorkRoot - SSL/HTTPS" "https://workroot.in/" "1" "300" "$ALERT_ID"
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Setup complete!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Login to UptimeRobot: https://uptimerobot.com/dashboard"
|
||||
echo " 2. Enable SSL certificate expiry alerts for each HTTPS monitor"
|
||||
echo " Monitor → Edit → Enable SSL monitoring → Set alert threshold to 14 days"
|
||||
echo " 3. Configure response time alert threshold:"
|
||||
echo " Monitor → Edit → Response Time Threshold → 3000ms"
|
||||
echo " 4. Set up status page: Dashboard → Status Pages → Create"
|
||||
echo "============================================"
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Sitemap Testing Script
|
||||
# Validates sitemap.xml structure and content
|
||||
# Usage: ./scripts/test-sitemap.sh
|
||||
|
||||
set -e
|
||||
|
||||
SITEMAP_URL="http://localhost:10000/sitemap.xml"
|
||||
PROD_SITEMAP_URL="https://workroot.in/sitemap.xml"
|
||||
|
||||
echo "🧪 Testing Sitemap..."
|
||||
echo ""
|
||||
|
||||
# Test 1: Sitemap accessible
|
||||
echo "1️⃣ Testing sitemap accessibility..."
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$SITEMAP_URL")
|
||||
if [ "$HTTP_CODE" = "200" ]; then
|
||||
echo " ✅ Sitemap is accessible (HTTP $HTTP_CODE)"
|
||||
else
|
||||
echo " ❌ Sitemap returned HTTP $HTTP_CODE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 2: Valid XML
|
||||
echo "2️⃣ Testing XML validity..."
|
||||
if curl -s "$SITEMAP_URL" | xmllint --noout - 2>/dev/null; then
|
||||
echo " ✅ Sitemap is valid XML"
|
||||
else
|
||||
echo " ⚠️ XML validation failed (xmllint not installed or XML invalid)"
|
||||
fi
|
||||
|
||||
# Test 3: Count URLs
|
||||
echo "3️⃣ Counting URLs..."
|
||||
URL_COUNT=$(curl -s "$SITEMAP_URL" | grep -c '<loc>' || echo "0")
|
||||
echo " 📊 Found $URL_COUNT URLs in sitemap"
|
||||
|
||||
# Test 4: Check for blog posts
|
||||
echo "4️⃣ Checking for blog posts..."
|
||||
BLOG_COUNT=$(curl -s "$SITEMAP_URL" | grep -c 'workroot.in/blog/[^<]*</loc>' || echo "0")
|
||||
if [ "$BLOG_COUNT" -gt 0 ]; then
|
||||
echo " ✅ Found $BLOG_COUNT blog posts"
|
||||
else
|
||||
echo " ⚠️ No blog posts found (might be normal if no published posts)"
|
||||
fi
|
||||
|
||||
# Test 5: Verify domain
|
||||
echo "5️⃣ Verifying domain..."
|
||||
if curl -s "$SITEMAP_URL" | grep -q 'workroot.in'; then
|
||||
echo " ✅ Correct domain (workroot.in)"
|
||||
else
|
||||
echo " ❌ Wrong domain found in sitemap"
|
||||
fi
|
||||
|
||||
# Test 6: Check lastmod dates
|
||||
echo "6️⃣ Checking lastmod dates..."
|
||||
LASTMOD_COUNT=$(curl -s "$SITEMAP_URL" | grep -c '<lastmod>' || echo "0")
|
||||
if [ "$LASTMOD_COUNT" -gt 0 ]; then
|
||||
echo " ✅ Found $LASTMOD_COUNT lastmod dates"
|
||||
else
|
||||
echo " ⚠️ No lastmod dates found"
|
||||
fi
|
||||
|
||||
# Test 7: Verify robots.txt reference
|
||||
echo "7️⃣ Checking robots.txt..."
|
||||
if curl -s "http://localhost:10000/robots.txt" | grep -q 'Sitemap:'; then
|
||||
echo " ✅ Sitemap referenced in robots.txt"
|
||||
else
|
||||
echo " ❌ Sitemap not in robots.txt"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📋 Summary:"
|
||||
echo " Total URLs: $URL_COUNT"
|
||||
echo " Blog posts: $BLOG_COUNT"
|
||||
echo " Lastmod dates: $LASTMOD_COUNT"
|
||||
echo ""
|
||||
echo "✨ Sitemap test complete!"
|
||||
echo ""
|
||||
echo "To view sitemap:"
|
||||
echo " Local: curl $SITEMAP_URL"
|
||||
echo " Prod: curl $PROD_SITEMAP_URL"
|
||||
@@ -0,0 +1,341 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Schema Validation Script
|
||||
*
|
||||
* Validates JSON-LD structured data on all pages
|
||||
* Checks for common issues and provides recommendations
|
||||
*
|
||||
* Usage: node scripts/validate-schema.js
|
||||
*/
|
||||
|
||||
import { JSDOM } from 'jsdom';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const COLORS = {
|
||||
reset: '\x1b[0m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
cyan: '\x1b[36m',
|
||||
};
|
||||
|
||||
const log = {
|
||||
success: (msg) => console.log(`${COLORS.green}✓${COLORS.reset} ${msg}`),
|
||||
error: (msg) => console.log(`${COLORS.red}✗${COLORS.reset} ${msg}`),
|
||||
warning: (msg) => console.log(`${COLORS.yellow}⚠${COLORS.reset} ${msg}`),
|
||||
info: (msg) => console.log(`${COLORS.blue}ℹ${COLORS.reset} ${msg}`),
|
||||
section: (msg) => console.log(`\n${COLORS.cyan}${msg}${COLORS.reset}\n`),
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract JSON-LD scripts from HTML
|
||||
*/
|
||||
function extractJsonLd(html) {
|
||||
const dom = new JSDOM(html);
|
||||
const scripts = dom.window.document.querySelectorAll('script[type="application/ld+json"]');
|
||||
return Array.from(scripts).map(script => {
|
||||
try {
|
||||
return JSON.parse(script.textContent);
|
||||
} catch (e) {
|
||||
log.error(`Failed to parse JSON-LD: ${e.message}`);
|
||||
return null;
|
||||
}
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate required fields for each schema type
|
||||
*/
|
||||
function validateSchema(schema) {
|
||||
const issues = [];
|
||||
const warnings = [];
|
||||
|
||||
if (!schema['@context']) {
|
||||
issues.push('Missing @context');
|
||||
}
|
||||
|
||||
if (!schema['@type']) {
|
||||
issues.push('Missing @type');
|
||||
}
|
||||
|
||||
const type = schema['@type'];
|
||||
|
||||
// Validate based on type
|
||||
switch (type) {
|
||||
case 'Organization':
|
||||
if (!schema.name) issues.push('Organization: Missing name');
|
||||
if (!schema.url) issues.push('Organization: Missing url');
|
||||
if (!schema.logo) warnings.push('Organization: Consider adding logo');
|
||||
if (!schema.contactPoint) warnings.push('Organization: Consider adding contactPoint');
|
||||
break;
|
||||
|
||||
case 'BlogPosting':
|
||||
if (!schema.headline) issues.push('BlogPosting: Missing headline');
|
||||
if (!schema.author) issues.push('BlogPosting: Missing author');
|
||||
if (!schema.datePublished) issues.push('BlogPosting: Missing datePublished');
|
||||
if (!schema.publisher) issues.push('BlogPosting: Missing publisher');
|
||||
if (!schema.image) warnings.push('BlogPosting: Consider adding image');
|
||||
break;
|
||||
|
||||
case 'BreadcrumbList':
|
||||
if (!schema.itemListElement || !Array.isArray(schema.itemListElement)) {
|
||||
issues.push('BreadcrumbList: Missing or invalid itemListElement');
|
||||
} else {
|
||||
schema.itemListElement.forEach((item, idx) => {
|
||||
if (!item.position) issues.push(`BreadcrumbList: Item ${idx} missing position`);
|
||||
if (!item.name) issues.push(`BreadcrumbList: Item ${idx} missing name`);
|
||||
if (!item.item) issues.push(`BreadcrumbList: Item ${idx} missing item URL`);
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'WebPage':
|
||||
if (!schema.name) issues.push('WebPage: Missing name');
|
||||
if (!schema.url) issues.push('WebPage: Missing url');
|
||||
if (!schema.description) warnings.push('WebPage: Consider adding description');
|
||||
break;
|
||||
|
||||
case 'FAQPage':
|
||||
if (!schema.mainEntity || !Array.isArray(schema.mainEntity)) {
|
||||
issues.push('FAQPage: Missing or invalid mainEntity');
|
||||
} else {
|
||||
schema.mainEntity.forEach((item, idx) => {
|
||||
if (item['@type'] !== 'Question') {
|
||||
issues.push(`FAQPage: Item ${idx} should be type Question`);
|
||||
}
|
||||
if (!item.name) issues.push(`FAQPage: Question ${idx} missing name`);
|
||||
if (!item.acceptedAnswer) {
|
||||
issues.push(`FAQPage: Question ${idx} missing acceptedAnswer`);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Service':
|
||||
if (!schema.name) issues.push('Service: Missing name');
|
||||
if (!schema.description) issues.push('Service: Missing description');
|
||||
if (!schema.provider) warnings.push('Service: Consider adding provider');
|
||||
break;
|
||||
|
||||
case 'WebSite':
|
||||
if (!schema.name) issues.push('WebSite: Missing name');
|
||||
if (!schema.url) issues.push('WebSite: Missing url');
|
||||
if (!schema.potentialAction) warnings.push('WebSite: Consider adding search action');
|
||||
break;
|
||||
}
|
||||
|
||||
return { issues, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Open Graph tags
|
||||
*/
|
||||
function validateOpenGraph(html) {
|
||||
const dom = new JSDOM(html);
|
||||
const doc = dom.window.document;
|
||||
const issues = [];
|
||||
const warnings = [];
|
||||
|
||||
const requiredOgTags = ['og:title', 'og:description', 'og:image', 'og:url', 'og:type'];
|
||||
|
||||
requiredOgTags.forEach(tag => {
|
||||
const meta = doc.querySelector(`meta[property="${tag}"]`);
|
||||
if (!meta) {
|
||||
issues.push(`Missing required Open Graph tag: ${tag}`);
|
||||
}
|
||||
});
|
||||
|
||||
const ogImage = doc.querySelector('meta[property="og:image"]');
|
||||
if (ogImage) {
|
||||
const imageUrl = ogImage.getAttribute('content');
|
||||
if (!imageUrl.startsWith('http')) {
|
||||
warnings.push('og:image should be an absolute URL');
|
||||
}
|
||||
if (!doc.querySelector('meta[property="og:image:width"]')) {
|
||||
warnings.push('Consider adding og:image:width');
|
||||
}
|
||||
if (!doc.querySelector('meta[property="og:image:height"]')) {
|
||||
warnings.push('Consider adding og:image:height');
|
||||
}
|
||||
}
|
||||
|
||||
return { issues, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Twitter Card tags
|
||||
*/
|
||||
function validateTwitterCard(html) {
|
||||
const dom = new JSDOM(html);
|
||||
const doc = dom.window.document;
|
||||
const issues = [];
|
||||
const warnings = [];
|
||||
|
||||
const requiredTwitterTags = ['twitter:card', 'twitter:title', 'twitter:description', 'twitter:image'];
|
||||
|
||||
requiredTwitterTags.forEach(tag => {
|
||||
const meta = doc.querySelector(`meta[name="${tag}"]`);
|
||||
if (!meta) {
|
||||
issues.push(`Missing required Twitter Card tag: ${tag}`);
|
||||
}
|
||||
});
|
||||
|
||||
const twitterCard = doc.querySelector('meta[name="twitter:card"]');
|
||||
if (twitterCard && twitterCard.getAttribute('content') === 'summary_large_image') {
|
||||
const twitterImage = doc.querySelector('meta[name="twitter:image"]');
|
||||
if (twitterImage) {
|
||||
const imageUrl = twitterImage.getAttribute('content');
|
||||
if (!imageUrl.startsWith('http')) {
|
||||
warnings.push('twitter:image should be an absolute URL');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { issues, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Main validation function
|
||||
*/
|
||||
async function validatePage(filePath, pageName) {
|
||||
log.section(`Validating: ${pageName}`);
|
||||
|
||||
try {
|
||||
const html = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// Extract and validate JSON-LD
|
||||
const schemas = extractJsonLd(html);
|
||||
|
||||
if (schemas.length === 0) {
|
||||
log.warning('No JSON-LD structured data found');
|
||||
} else {
|
||||
log.info(`Found ${schemas.length} JSON-LD schema(s)`);
|
||||
|
||||
schemas.forEach((schema, idx) => {
|
||||
console.log(`\n Schema ${idx + 1}: ${schema['@type'] || 'Unknown'}`);
|
||||
const { issues, warnings } = validateSchema(schema);
|
||||
|
||||
if (issues.length === 0) {
|
||||
log.success('No critical issues');
|
||||
} else {
|
||||
issues.forEach(issue => log.error(` ${issue}`));
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
warnings.forEach(warning => log.warning(` ${warning}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Validate Open Graph
|
||||
console.log('\n Open Graph:');
|
||||
const ogValidation = validateOpenGraph(html);
|
||||
if (ogValidation.issues.length === 0) {
|
||||
log.success('All required Open Graph tags present');
|
||||
} else {
|
||||
ogValidation.issues.forEach(issue => log.error(` ${issue}`));
|
||||
}
|
||||
if (ogValidation.warnings.length > 0) {
|
||||
ogValidation.warnings.forEach(warning => log.warning(` ${warning}`));
|
||||
}
|
||||
|
||||
// Validate Twitter Cards
|
||||
console.log('\n Twitter Card:');
|
||||
const twitterValidation = validateTwitterCard(html);
|
||||
if (twitterValidation.issues.length === 0) {
|
||||
log.success('All required Twitter Card tags present');
|
||||
} else {
|
||||
twitterValidation.issues.forEach(issue => log.error(` ${issue}`));
|
||||
}
|
||||
if (twitterValidation.warnings.length > 0) {
|
||||
twitterValidation.warnings.forEach(warning => log.warning(` ${warning}`));
|
||||
}
|
||||
|
||||
return {
|
||||
schemas: schemas.length,
|
||||
issues: ogValidation.issues.length + twitterValidation.issues.length,
|
||||
warnings: ogValidation.warnings.length + twitterValidation.warnings.length,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
log.error(`Failed to validate ${pageName}: ${error.message}`);
|
||||
return { schemas: 0, issues: 1, warnings: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution
|
||||
*/
|
||||
async function main() {
|
||||
console.log('\n╔════════════════════════════════════════════════════════╗');
|
||||
console.log('║ Schema Validation for WorkRoot IT Solutions ║');
|
||||
console.log('╚════════════════════════════════════════════════════════╝\n');
|
||||
|
||||
const distDir = path.join(__dirname, '..', 'dist');
|
||||
|
||||
if (!fs.existsSync(distDir)) {
|
||||
log.error('Build directory not found. Please run "npm run build" first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Pages to validate
|
||||
const pages = [
|
||||
{ path: path.join(distDir, 'index.html'), name: 'Homepage' },
|
||||
{ path: path.join(distDir, 'about', 'index.html'), name: 'About' },
|
||||
{ path: path.join(distDir, 'services', 'index.html'), name: 'Services' },
|
||||
{ path: path.join(distDir, 'contact', 'index.html'), name: 'Contact' },
|
||||
{ path: path.join(distDir, 'blog', 'index.html'), name: 'Blog Index' },
|
||||
];
|
||||
|
||||
// Find a blog post to validate
|
||||
const blogDir = path.join(distDir, 'blog');
|
||||
if (fs.existsSync(blogDir)) {
|
||||
const blogPosts = fs.readdirSync(blogDir)
|
||||
.filter(file => fs.statSync(path.join(blogDir, file)).isDirectory());
|
||||
|
||||
if (blogPosts.length > 0) {
|
||||
pages.push({
|
||||
path: path.join(blogDir, blogPosts[0], 'index.html'),
|
||||
name: `Blog Post (${blogPosts[0]})`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let totalSchemas = 0;
|
||||
let totalIssues = 0;
|
||||
let totalWarnings = 0;
|
||||
|
||||
for (const page of pages) {
|
||||
if (fs.existsSync(page.path)) {
|
||||
const result = await validatePage(page.path, page.name);
|
||||
totalSchemas += result.schemas;
|
||||
totalIssues += result.issues;
|
||||
totalWarnings += result.warnings;
|
||||
} else {
|
||||
log.warning(`Page not found: ${page.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
log.section('Validation Summary');
|
||||
console.log(`Total schemas found: ${COLORS.cyan}${totalSchemas}${COLORS.reset}`);
|
||||
console.log(`Total issues: ${totalIssues > 0 ? COLORS.red : COLORS.green}${totalIssues}${COLORS.reset}`);
|
||||
console.log(`Total warnings: ${totalWarnings > 0 ? COLORS.yellow : COLORS.green}${totalWarnings}${COLORS.reset}`);
|
||||
|
||||
if (totalIssues === 0) {
|
||||
log.success('\nAll validations passed! 🎉');
|
||||
} else {
|
||||
log.error('\nPlease fix the issues above before deployment.');
|
||||
}
|
||||
|
||||
console.log('\n');
|
||||
process.exit(totalIssues > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user