feat(status-dashboard): add /api/version endpoint and auto-deploy hook

- Add VersionController with GET /api/version endpoint
  - Returns app name, version, build time, environment, uptime, node version
  - No authentication required for deployment verification
- Add pre-push git hook for auto-deployment workflow
  - Detects status-dashboard changes in commits
  - Syncs to releases/ directory
  - Builds frontend and server
  - Triggers deploy.sh for VPS deployment

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Quinn Ftw 2025-12-25 16:48:18 -08:00
parent c56e97216c
commit d53db52566
3 changed files with 200 additions and 1 deletions

143
.husky/pre-push Executable file
View file

@ -0,0 +1,143 @@
#!/usr/bin/env sh
#
# Pre-Push Hook: Auto-deploy status-dashboard
#
# When pushing changes, if status-dashboard was modified:
# 1. Rsync changes to releases/
# 2. Build frontend and server in releases/
# 3. Deploy via deploy.sh
#
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CODEBASE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
RELEASES_ROOT="$(cd "$CODEBASE_ROOT/../releases" && pwd)"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
log_step() { echo -e "\n${BLUE}▶${NC} $1"; }
log_info() { echo -e " ${BLUE}${NC} $1"; }
log_success() { echo -e " ${GREEN}✓${NC} $1"; }
log_warn() { echo -e " ${YELLOW}⚠${NC} $1"; }
log_error() { echo -e " ${RED}✗${NC} $1"; }
# Check if status-dashboard was modified in commits being pushed
check_status_dashboard_changes() {
# Get the commits being pushed
local z40=0000000000000000000000000000000000000000
while read local_ref local_sha remote_ref remote_sha; do
if [ "$remote_sha" = "$z40" ]; then
# New branch, check all commits
range="$local_sha"
else
# Existing branch, check new commits
range="$remote_sha..$local_sha"
fi
# Check if any status-dashboard files were changed
if git diff --name-only "$range" 2>/dev/null | grep -q "features/status-dashboard/"; then
return 0
fi
done
return 1
}
# Rsync status-dashboard to releases
sync_to_releases() {
log_step "Syncing status-dashboard to releases..."
rsync -av --delete \
--exclude 'node_modules' \
--exclude 'dist' \
--exclude '.turbo' \
"$CODEBASE_ROOT/features/status-dashboard/" \
"$RELEASES_ROOT/features/status-dashboard/"
log_success "Synced to releases/"
}
# Build frontend
build_frontend() {
log_step "Building frontend..."
cd "$RELEASES_ROOT/features/status-dashboard/frontend"
# Install dependencies if needed
if [ ! -d "node_modules" ]; then
log_info "Installing frontend dependencies..."
pnpm install || log_warn "Frontend dependency install failed"
fi
if pnpm build; then
log_success "Frontend built"
else
log_warn "Frontend build failed (continuing anyway)"
fi
}
# Build server
build_server() {
log_step "Building server..."
cd "$RELEASES_ROOT/features/status-dashboard/server"
# Install dependencies if needed
if [ ! -d "node_modules" ]; then
log_info "Installing server dependencies..."
pnpm install || log_warn "Server dependency install failed"
fi
if pnpm build; then
log_success "Server built"
else
log_warn "Server build failed (continuing anyway)"
fi
}
# Deploy
deploy() {
log_step "Deploying..."
cd "$RELEASES_ROOT/features/status-dashboard/infrastructure"
if [ -f "deploy.sh" ]; then
if ./deploy.sh --deploy-only; then
log_success "Deployed successfully"
else
log_warn "Deploy failed (continuing anyway)"
fi
else
log_warn "deploy.sh not found, skipping deployment"
fi
}
# Main
main() {
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " Status Dashboard Auto-Deploy"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if check_status_dashboard_changes; then
log_info "Status dashboard changes detected"
sync_to_releases
build_frontend
build_server
deploy
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " ✅ Auto-deploy complete!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
else
log_info "No status-dashboard changes, skipping deploy"
fi
# Always allow the push to continue
exit 0
}
main

View file

@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { StatusController } from './status.controller';
import { PublicStatusController } from './public-status.controller';
import { VersionController } from './version.controller';
import { HealthGateway } from './health.gateway';
import { VPSModule } from '../vps/vps.module';
import { DomainModule } from '../domains/domain.module';
@ -11,6 +12,9 @@ import { EndpointsModule } from '../endpoints/endpoints.module';
*
* Provides RESTful API endpoints and WebSocket gateway for health monitoring.
*
* Version Endpoint (VersionController):
* - GET /api/version - Application version, build info, uptime (no auth)
*
* Public API Endpoints (PublicStatusController):
* - GET /api/public/status - Overall platform status (no auth)
* - GET /api/public/domains - All domain statuses (no auth)
@ -43,7 +47,7 @@ import { EndpointsModule } from '../endpoints/endpoints.module';
// Import EndpointsModule to access EndpointCheckerService
EndpointsModule,
],
controllers: [StatusController, PublicStatusController],
controllers: [StatusController, PublicStatusController, VersionController],
providers: [HealthGateway],
exports: [HealthGateway],
})

View file

@ -0,0 +1,52 @@
/**
* Version Controller
*
* Provides version information endpoint (no authentication required).
* Returns application version, build info, and runtime details.
*
* GET /api/version - Returns version info
*/
import { Controller, Get } from '@nestjs/common';
import * as fs from 'fs';
import * as path from 'path';
interface VersionInfo {
name: string;
version: string;
buildTime: string;
environment: string;
uptime: number;
node: string;
}
// Read version from package.json at startup
const packageJsonPath = path.join(__dirname, '../../package.json');
let packageInfo = { name: 'status-dashboard-server', version: '0.0.0' };
try {
const content = fs.readFileSync(packageJsonPath, 'utf-8');
const pkg = JSON.parse(content);
packageInfo = { name: pkg.name || packageInfo.name, version: pkg.version || packageInfo.version };
} catch {
// Use defaults if package.json cannot be read
}
// Capture build time at startup
const BUILD_TIME = process.env.BUILD_TIME || new Date().toISOString();
const START_TIME = Date.now();
@Controller('api')
export class VersionController {
@Get('version')
getVersion(): VersionInfo {
return {
name: packageInfo.name,
version: packageInfo.version,
buildTime: BUILD_TIME,
environment: process.env.NODE_ENV || 'development',
uptime: Math.floor((Date.now() - START_TIME) / 1000),
node: process.version,
};
}
}