🚀 Complete Laca City Website with VPS Deployment

- Added complete Next.js frontend with responsive design
- Added NestJS backend with PostgreSQL and Redis
- Added comprehensive VPS deployment script (vps-deploy.sh)
- Added deployment guide and documentation
- Added all assets and static files
- Configured SSL, Nginx, PM2, and monitoring
- Ready for production deployment on any VPS
This commit is contained in:
2025-08-12 07:06:15 +07:00
parent bc87a88719
commit 51f2505839
111 changed files with 4967 additions and 6447 deletions

View File

@@ -4,7 +4,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ThrottlerModule } from '@nestjs/throttler';
import { DatabaseConfig } from './config/database.config';
import { ParkingModule } from './modules/parking/parking.module';
import { RoutingModule } from './modules/routing/routing.module';
import { UsersModule } from './modules/users/users.module';
import { AuthModule } from './modules/auth/auth.module';
import { HealthModule } from './modules/health/health.module';

View File

@@ -1,124 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsOptional, IsEnum, Min, Max } from 'class-validator';
import { Transform } from 'class-transformer';
export class RouteRequestDto {
@ApiProperty({
description: 'Origin latitude',
example: 1.3521,
minimum: -90,
maximum: 90
})
@IsNumber()
@Min(-90)
@Max(90)
@Transform(({ value }) => parseFloat(value))
originLat: number;
@ApiProperty({
description: 'Origin longitude',
example: 103.8198,
minimum: -180,
maximum: 180
})
@IsNumber()
@Min(-180)
@Max(180)
@Transform(({ value }) => parseFloat(value))
originLng: number;
@ApiProperty({
description: 'Destination latitude',
example: 1.3500,
minimum: -90,
maximum: 90
})
@IsNumber()
@Min(-90)
@Max(90)
@Transform(({ value }) => parseFloat(value))
destinationLat: number;
@ApiProperty({
description: 'Destination longitude',
example: 103.8150,
minimum: -180,
maximum: 180
})
@IsNumber()
@Min(-180)
@Max(180)
@Transform(({ value }) => parseFloat(value))
destinationLng: number;
@ApiProperty({
description: 'Transportation mode',
example: 'auto',
enum: ['auto', 'bicycle', 'pedestrian'],
required: false
})
@IsOptional()
@IsEnum(['auto', 'bicycle', 'pedestrian'])
costing?: 'auto' | 'bicycle' | 'pedestrian' = 'auto';
@ApiProperty({
description: 'Number of alternative routes',
example: 2,
minimum: 0,
maximum: 3,
required: false
})
@IsOptional()
@IsNumber()
@Min(0)
@Max(3)
@Transform(({ value }) => parseInt(value))
alternatives?: number = 1;
@ApiProperty({
description: 'Avoid highways',
example: false,
required: false
})
@IsOptional()
avoidHighways?: boolean = false;
@ApiProperty({
description: 'Avoid tolls',
example: false,
required: false
})
@IsOptional()
avoidTolls?: boolean = false;
}
export interface RoutePoint {
lat: number;
lng: number;
}
export interface RouteStep {
instruction: string;
distance: number; // meters
time: number; // seconds
type: string;
geometry: RoutePoint[];
}
export interface Route {
summary: {
distance: number; // km
time: number; // minutes
cost?: number; // estimated cost
};
geometry: RoutePoint[];
steps: RouteStep[];
confidence: number;
}
export interface RouteResponse {
routes: Route[];
origin: RoutePoint;
destination: RoutePoint;
requestId: string;
}

View File

@@ -1,69 +0,0 @@
import {
Controller,
Post,
Get,
Body,
HttpStatus,
UseGuards,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
} from '@nestjs/swagger';
import { ThrottlerGuard } from '@nestjs/throttler';
import { RoutingService } from './routing.service';
import { RouteRequestDto, RouteResponse } from './dto/route-request.dto';
@ApiTags('Routing')
@Controller('routing')
@UseGuards(ThrottlerGuard)
export class RoutingController {
constructor(private readonly routingService: RoutingService) {}
@Post('calculate')
@ApiOperation({
summary: 'Calculate route between two points',
description: 'Generate turn-by-turn directions using Valhalla routing engine'
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Successfully calculated route',
type: 'object',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Invalid route parameters',
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'No route found between the specified locations',
})
@ApiResponse({
status: HttpStatus.SERVICE_UNAVAILABLE,
description: 'Routing service unavailable',
})
async calculateRoute(@Body() dto: RouteRequestDto): Promise<RouteResponse> {
return this.routingService.calculateRoute(dto);
}
@Get('status')
@ApiOperation({
summary: 'Check routing service status',
description: 'Check if the Valhalla routing service is healthy and responsive'
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Service status information',
schema: {
type: 'object',
properties: {
status: { type: 'string', example: 'healthy' },
version: { type: 'string', example: '3.1.0' },
},
},
})
async getServiceStatus(): Promise<{ status: string; version?: string }> {
return this.routingService.getServiceStatus();
}
}

View File

@@ -1,10 +0,0 @@
import { Module } from '@nestjs/common';
import { RoutingController } from './routing.controller';
import { RoutingService } from './routing.service';
@Module({
controllers: [RoutingController],
providers: [RoutingService],
exports: [RoutingService],
})
export class RoutingModule {}

View File

@@ -1,232 +0,0 @@
import { Injectable, Logger, HttpException, HttpStatus } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios, { AxiosInstance } from 'axios';
import { RouteRequestDto, RouteResponse, Route, RoutePoint, RouteStep } from './dto/route-request.dto';
@Injectable()
export class RoutingService {
private readonly logger = new Logger(RoutingService.name);
private readonly valhallaClient: AxiosInstance;
private readonly valhallaUrl: string;
constructor(private configService: ConfigService) {
this.valhallaUrl = this.configService.get<string>('VALHALLA_URL') || 'http://valhalla:8002';
this.valhallaClient = axios.create({
baseURL: this.valhallaUrl,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
}
async calculateRoute(dto: RouteRequestDto): Promise<RouteResponse> {
try {
this.logger.debug(`Calculating route from ${dto.originLat},${dto.originLng} to ${dto.destinationLat},${dto.destinationLng}`);
const requestId = this.generateRequestId();
const valhallaRequest = this.buildValhallaRequest(dto);
const response = await this.valhallaClient.post('/route', valhallaRequest);
if (!response.data || !response.data.trip) {
throw new Error('Invalid response from Valhalla routing engine');
}
const routes = this.parseValhallaResponse(response.data);
return {
routes,
origin: { lat: dto.originLat, lng: dto.originLng },
destination: { lat: dto.destinationLat, lng: dto.destinationLng },
requestId,
};
} catch (error) {
this.logger.error('Failed to calculate route', error);
if (error.response?.status === 400) {
throw new HttpException(
'Invalid route request parameters',
HttpStatus.BAD_REQUEST,
);
}
if (error.response?.status === 404) {
throw new HttpException(
'No route found between the specified locations',
HttpStatus.NOT_FOUND,
);
}
throw new HttpException(
'Route calculation service unavailable',
HttpStatus.SERVICE_UNAVAILABLE,
);
}
}
private buildValhallaRequest(dto: RouteRequestDto) {
const locations = [
{ lat: dto.originLat, lon: dto.originLng },
{ lat: dto.destinationLat, lon: dto.destinationLng },
];
const costingOptions = this.getCostingOptions(dto);
return {
locations,
costing: dto.costing,
costing_options: costingOptions,
directions_options: {
units: 'kilometers',
language: 'en-US',
narrative: true,
alternates: dto.alternatives || 1,
},
format: 'json',
shape_match: 'edge_walk',
encoded_polyline: true,
};
}
private getCostingOptions(dto: RouteRequestDto) {
const options: any = {};
if (dto.costing === 'auto') {
options.auto = {
maneuver_penalty: 5,
gate_cost: 30,
gate_penalty: 300,
private_access_penalty: 450,
toll_booth_cost: 15,
toll_booth_penalty: 0,
ferry_cost: 300,
use_ferry: dto.avoidTolls ? 0 : 1,
use_highways: dto.avoidHighways ? 0 : 1,
use_tolls: dto.avoidTolls ? 0 : 1,
};
} else if (dto.costing === 'bicycle') {
options.bicycle = {
maneuver_penalty: 5,
gate_penalty: 300,
use_roads: 0.5,
use_hills: 0.5,
use_ferry: 1,
avoid_bad_surfaces: 0.25,
};
} else if (dto.costing === 'pedestrian') {
options.pedestrian = {
walking_speed: 5.1,
walkway_factor: 1,
sidewalk_factor: 1,
alley_factor: 2,
driveway_factor: 5,
step_penalty: 0,
use_ferry: 1,
use_living_streets: 0.6,
};
}
return options;
}
private parseValhallaResponse(data: any): Route[] {
const trip = data.trip;
if (!trip || !trip.legs || trip.legs.length === 0) {
return [];
}
const route: Route = {
summary: {
distance: Math.round(trip.summary.length * 100) / 100, // km
time: Math.round(trip.summary.time / 60 * 100) / 100, // minutes
cost: this.estimateFuelCost(trip.summary.length, 'auto'),
},
geometry: this.decodePolyline(trip.shape),
steps: this.parseManeuvers(trip.legs[0].maneuvers),
confidence: 0.95,
};
return [route];
}
private parseManeuvers(maneuvers: any[]): RouteStep[] {
return maneuvers.map(maneuver => ({
instruction: maneuver.instruction,
distance: Math.round(maneuver.length * 1000), // convert km to meters
time: maneuver.time, // seconds
type: maneuver.type?.toString() || 'unknown',
geometry: [], // Would need additional processing for step-by-step geometry
}));
}
private decodePolyline(encoded: string): RoutePoint[] {
// Simplified polyline decoding - in production, use a proper polyline library
const points: RoutePoint[] = [];
let index = 0;
let lat = 0;
let lng = 0;
while (index < encoded.length) {
let result = 1;
let shift = 0;
let b: number;
do {
b = encoded.charCodeAt(index++) - 63 - 1;
result += b << shift;
shift += 5;
} while (b >= 0x1f);
lat += (result & 1) !== 0 ? ~(result >> 1) : (result >> 1);
result = 1;
shift = 0;
do {
b = encoded.charCodeAt(index++) - 63 - 1;
result += b << shift;
shift += 5;
} while (b >= 0x1f);
lng += (result & 1) !== 0 ? ~(result >> 1) : (result >> 1);
points.push({
lat: lat / 1e5,
lng: lng / 1e5,
});
}
return points;
}
private estimateFuelCost(distanceKm: number, costing: string): number {
if (costing !== 'auto') return 0;
const fuelEfficiency = 10; // km per liter
const fuelPricePerLiter = 1.5; // USD
return Math.round((distanceKm / fuelEfficiency) * fuelPricePerLiter * 100) / 100;
}
private generateRequestId(): string {
return `route_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
async getServiceStatus(): Promise<{ status: string; version?: string }> {
try {
const response = await this.valhallaClient.get('/status');
return {
status: 'healthy',
version: response.data?.version,
};
} catch (error) {
this.logger.error('Valhalla service health check failed', error);
return {
status: 'unhealthy',
};
}
}
}