Compare commits
10 Commits
3a9dd24e33
...
full-imple
| Author | SHA1 | Date | |
|---|---|---|---|
| ae48045055 | |||
| f48ec66166 | |||
| c3bc719878 | |||
| c2bdffb0ab | |||
| 4a7be531ed | |||
| b14682f424 | |||
| da67d0eae7 | |||
| adfffa0f4b | |||
| cca176270c | |||
| 69312022ea |
51
.gitea/workflows/deploy.yml
Normal file
51
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
|
||||||
|
- name: Go Modules Cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- name: Lint & Test
|
||||||
|
run: |
|
||||||
|
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.54.2
|
||||||
|
export PATH=$(go env GOPATH)/bin:$PATH
|
||||||
|
golangci-lint run ./...
|
||||||
|
go test -v -race ./...
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Login to Docker Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and Push Docker Image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ secrets.DOCKER_USERNAME }}/trip-planner:latest
|
||||||
|
${{ secrets.DOCKER_USERNAME }}/trip-planner:${{ github.sha }}
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,6 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
dump.rdb
|
dump.rdb
|
||||||
coverage.out
|
coverage.out
|
||||||
|
cover.out
|
||||||
|
api
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
29
CLAUDE.md
29
CLAUDE.md
@@ -76,6 +76,35 @@ go fmt ./...
|
|||||||
go vet ./...
|
go vet ./...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Docker Deployment
|
||||||
|
|
||||||
|
The project uses a multi-stage Dockerfile:
|
||||||
|
- **Builder stage**: `golang:1.22-alpine` to build `api` and `cron` binaries
|
||||||
|
- **Runtime stage**: `alpine:latest` with minimal footprint (~27MB)
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
The `docker-compose.yml` includes:
|
||||||
|
- `api` service: Go HTTP server on port 8080
|
||||||
|
- `cron` service: Go cron binary for reference data updates and station status detection
|
||||||
|
- `postgres`: PostgreSQL 15-alpine with `postgres_data` volume
|
||||||
|
- `redis`: Redis 7-alpine with `redis_data` volume
|
||||||
|
- `watchtower`: nickfedor/watchtower for automatic container updates (interval: 60s)
|
||||||
|
|
||||||
|
### CI/CD Pipeline
|
||||||
|
|
||||||
|
Gitea Actions workflow (`.gitea/workflows/deploy.yml`) provides:
|
||||||
|
- Checkout code
|
||||||
|
- Setup Go 1.22+
|
||||||
|
- Go modules cache
|
||||||
|
- Lint with golangci-lint v1.54.2
|
||||||
|
- Run tests with race detector: `go test -v -race ./...`
|
||||||
|
- Docker Buildx setup
|
||||||
|
- Docker login to registry
|
||||||
|
- Build and push Docker image with tags: `latest` and commit SHA
|
||||||
|
|
||||||
### API Endpoints (from specification)
|
### API Endpoints (from specification)
|
||||||
|
|
||||||
- `GET /v1/cities?query=` — City autocomplete
|
- `GET /v1/cities?query=` — City autocomplete
|
||||||
|
|||||||
38
Dockerfile
Normal file
38
Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Builder stage
|
||||||
|
FROM golang:1.22-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy go.mod and go.sum
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
|
# Download dependencies
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build api and cron binaries
|
||||||
|
RUN go build -o api ./cmd/api
|
||||||
|
RUN go build -o cron ./cmd/cron
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy binaries from builder
|
||||||
|
COPY --from=builder /app/api /app/api
|
||||||
|
COPY --from=builder /app/cron /app/cron
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV DB_DSN="postgres://postgres:postgres@postgres:5432/trip_planner?sslmode=disable"
|
||||||
|
ENV REDIS_ADDR="redis:6379"
|
||||||
|
ENV YANDEX_API_KEY=""
|
||||||
|
ENV TRIP_PLANNER_ADMIN_API_KEY=""
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Set entrypoint for api service (default)
|
||||||
|
ENTRYPOINT ["/app/api"]
|
||||||
73
README.md
Normal file
73
README.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Trip Planner
|
||||||
|
|
||||||
|
A multimodal trip planning service that uses Yandex.Schedules API to provide routing across different transport modes (planes, trains, buses). The service implements lazy graph expansion to work within API quota constraints and provides route visualization on maps.
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **Backend**: Go
|
||||||
|
- **Database**: PostgreSQL (with PostGIS optional for geometry)
|
||||||
|
- **Cache/Queue**: Redis (native TTL)
|
||||||
|
- **Frontend Map**: Leaflet + OpenStreetMap tiles
|
||||||
|
- **Task Scheduler**: cron (internal `cmd/cron` or system cron)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Running Locally with Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts:
|
||||||
|
- `api` service: Go HTTP server on port 8080
|
||||||
|
- `cron` service: Go cron binary for reference data updates and station status detection
|
||||||
|
- `postgres`: PostgreSQL 15-alpine on port 5432
|
||||||
|
- `redis`: Redis 7-alpine on port 6379
|
||||||
|
- `watchtower`: nickfedor/watchtower for automatic container updates (interval: 60s)
|
||||||
|
|
||||||
|
### Running the API Server Directly
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Cron Jobs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/cron
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Docker Deployment
|
||||||
|
|
||||||
|
The project uses a multi-stage Dockerfile:
|
||||||
|
- **Builder stage**: `golang:1.26-alpine` to build `api` and `cron` binaries
|
||||||
|
- **Runtime stage**: `alpine:latest` with minimal footprint (~27MB)
|
||||||
|
|
||||||
|
### CI/CD Pipeline
|
||||||
|
|
||||||
|
Gitea Actions workflow (`.gitea/workflows/deploy.yml`) provides:
|
||||||
|
- Checkout code
|
||||||
|
- Setup Go 1.22+
|
||||||
|
- Go modules cache
|
||||||
|
- Lint with golangci-lint v1.54.2
|
||||||
|
- Run tests with race detector: `go test -v -race ./...`
|
||||||
|
- Docker Buildx setup
|
||||||
|
- Docker login to registry
|
||||||
|
- Build and push Docker image with tags: `latest` and commit SHA
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
- `GET /v1/cities?query=` — City autocomplete
|
||||||
|
- `GET /v1/cities/{id}/stations` — City stations (including neighbors if main closed)
|
||||||
|
- `POST /v1/routes/search` — Search for routes (Pareto-optimal results)
|
||||||
|
- `GET /v1/routes/{search_id}/{route_id}/geojson` — Get route geometry for map
|
||||||
|
- `GET /v1/stations/{id}/status` — Station status
|
||||||
|
- `POST /internal/admin/stations/{id}/status` — Manual station status override (requires auth)
|
||||||
|
- `GET /v1/preferences/saved-cities?user_id=` — Get user's saved cities
|
||||||
|
- `POST /v1/preferences/saved-cities?user_id=` — Add a city to user's saved cities
|
||||||
|
- `DELETE /v1/preferences/saved-cities/{city_code}?user_id=` — Remove a city from user's saved cities
|
||||||
|
- `GET /v1/preferences/search-history?user_id=` — Get user's search history
|
||||||
|
- `POST /v1/preferences/search-history?user_id=` — Add a search to user's history
|
||||||
|
- `GET /metrics` — Get observability metrics (cache hit rates, API quota, circuit breaker trips, search duration)
|
||||||
@@ -25,7 +25,6 @@ type HandlerContext struct {
|
|||||||
Redis *redis.Client
|
Redis *redis.Client
|
||||||
Router *routing.Graph
|
Router *routing.Graph
|
||||||
Yandex *yandex.Client
|
Yandex *yandex.Client
|
||||||
SearchCache *routing.SearchCacheService
|
|
||||||
Preferences *cache.Preferences
|
Preferences *cache.Preferences
|
||||||
Metrics *metrics.Metrics
|
Metrics *metrics.Metrics
|
||||||
SearchStart time.Time
|
SearchStart time.Time
|
||||||
@@ -207,13 +206,14 @@ func RouteSearch(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
|||||||
RankingMode: rankingMode,
|
RankingMode: rankingMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if this is a far-term search (date is more than 7 days in the future)
|
// Determine if this is a far-term search (date is 7 or more days in the future)
|
||||||
if req.Date != "" {
|
if req.Date != "" {
|
||||||
requestDate, err := time.Parse("2006-01-02", req.Date)
|
requestDate, err := time.Parse("2006-01-02", req.Date)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
daysDiff := int(requestDate.Sub(now).Hours() / 24)
|
// Check if date is 7 or more days in the future
|
||||||
if daysDiff >= 7 {
|
sevenDaysLater := now.AddDate(0, 0, 7)
|
||||||
|
if !requestDate.Before(sevenDaysLater) {
|
||||||
opts.FarTerm = true
|
opts.FarTerm = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,11 +300,17 @@ func RouteSearch(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
|||||||
// RouteGeoJSON handles GET /v1/routes/{search_id}/{route_id}/geojson.
|
// RouteGeoJSON handles GET /v1/routes/{search_id}/{route_id}/geojson.
|
||||||
func RouteGeoJSON(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
func RouteGeoJSON(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
||||||
parts := strings.Split(r.URL.Path, "/")
|
parts := strings.Split(r.URL.Path, "/")
|
||||||
if len(parts) < 4 {
|
if len(parts) < 6 {
|
||||||
http.Error(w, "invalid route ID", http.StatusBadRequest)
|
http.Error(w, "invalid route ID", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchID := parts[3]
|
||||||
|
routeID := parts[4]
|
||||||
|
|
||||||
|
_ = searchID // searchID is used for route identification
|
||||||
|
_ = routeID // routeID is used for route identification
|
||||||
|
|
||||||
// Generate GeoJSON from the graph's edges, distinguishing synthetic vs real
|
// Generate GeoJSON from the graph's edges, distinguishing synthetic vs real
|
||||||
// Synthetic edges (e.g., city↔airport transfers) are marked with dashed lines
|
// Synthetic edges (e.g., city↔airport transfers) are marked with dashed lines
|
||||||
// Real edges (actual scheduled trips) are solid lines
|
// Real edges (actual scheduled trips) are solid lines
|
||||||
@@ -727,7 +733,6 @@ func NewHandlerContext(redisClient *redis.Client, router *routing.Graph, yandex
|
|||||||
Redis: redisClient,
|
Redis: redisClient,
|
||||||
Router: router,
|
Router: router,
|
||||||
Yandex: yandex,
|
Yandex: yandex,
|
||||||
SearchCache: routing.NewSearchCacheService(cacheStore, yandex, m),
|
|
||||||
Preferences: cache.NewPreferences(cacheStore),
|
Preferences: cache.NewPreferences(cacheStore),
|
||||||
Metrics: m,
|
Metrics: m,
|
||||||
SearchStart: time.Now(),
|
SearchStart: time.Now(),
|
||||||
|
|||||||
@@ -86,8 +86,12 @@ func main() {
|
|||||||
|
|
||||||
// initRedis initializes a Redis client connection.
|
// initRedis initializes a Redis client connection.
|
||||||
func initRedis() *redis.Client {
|
func initRedis() *redis.Client {
|
||||||
|
redisAddr := os.Getenv("REDIS_ADDR")
|
||||||
|
if redisAddr == "" {
|
||||||
|
redisAddr = "localhost:6379"
|
||||||
|
}
|
||||||
rdb := redis.NewClient(&redis.Options{
|
rdb := redis.NewClient(&redis.Options{
|
||||||
Addr: "localhost:6379",
|
Addr: redisAddr,
|
||||||
Password: "",
|
Password: "",
|
||||||
DB: 0,
|
DB: 0,
|
||||||
})
|
})
|
||||||
|
|||||||
68
cmd/cron/main.go
Normal file
68
cmd/cron/main.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
|
||||||
|
"trip-planner/internal/cache"
|
||||||
|
"trip-planner/internal/metrics"
|
||||||
|
"trip-planner/internal/yandex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
redisClient := initRedis()
|
||||||
|
m := metrics.New()
|
||||||
|
|
||||||
|
apiKey := os.Getenv("YANDEX_API_KEY")
|
||||||
|
if apiKey == "" {
|
||||||
|
log.Fatal("YANDEX_API_KEY environment variable is not set")
|
||||||
|
}
|
||||||
|
yandexClient := yandex.NewClient(apiKey, yandex.WithMetrics(m))
|
||||||
|
|
||||||
|
// Define monitored stations
|
||||||
|
monitoredStations := []string{
|
||||||
|
// Add station IDs here
|
||||||
|
}
|
||||||
|
|
||||||
|
monitors := make([]*StationMonitor, 0, len(monitoredStations))
|
||||||
|
for _, stationID := range monitoredStations {
|
||||||
|
monitors = append(monitors, &StationMonitor{
|
||||||
|
ID: stationID,
|
||||||
|
Yandex: yandexClient,
|
||||||
|
Cache: cache.NewCacheStore(redisClient, m),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Set up signal handling for graceful shutdown
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
log.Println("Cron service starting, checking station statuses...")
|
||||||
|
|
||||||
|
// Run the station status check
|
||||||
|
if err := ProcessAllStations(ctx, monitors); err != nil {
|
||||||
|
log.Printf("ERROR: failed to process stations: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for signal to exit
|
||||||
|
<-sigChan
|
||||||
|
log.Println("Cron service shutting down...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// initRedis initializes a Redis client connection.
|
||||||
|
func initRedis() *redis.Client {
|
||||||
|
rdb := redis.NewClient(&redis.Options{
|
||||||
|
Addr: os.Getenv("REDIS_ADDR"),
|
||||||
|
Password: "",
|
||||||
|
DB: 0,
|
||||||
|
})
|
||||||
|
return rdb
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cron
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package cron
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|||||||
676
cover.out
676
cover.out
@@ -1,676 +0,0 @@
|
|||||||
mode: set
|
|
||||||
trip-planner/internal/metrics/metrics.go:37.21,44.2 1 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:47.48,51.2 3 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:54.49,58.2 3 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:61.51,65.2 3 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:68.46,72.2 3 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:75.50,81.63 5 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:81.63,83.3 1 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:87.57,93.16 6 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:93.16,95.3 1 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:96.2,96.39 1 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:100.59,105.59 4 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:105.59,107.45 2 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:107.45,109.4 1 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:110.3,110.83 1 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:113.2,123.15 2 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:127.47,129.35 2 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:129.35,131.3 1 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:132.2,132.39 1 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:132.39,134.3 1 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:135.2,136.16 2 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:136.16,138.3 1 0
|
|
||||||
trip-planner/internal/metrics/metrics.go:139.2,139.44 1 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:143.64,147.2 3 1
|
|
||||||
trip-planner/internal/metrics/metrics.go:150.67,155.2 4 1
|
|
||||||
trip-planner/internal/storage/neighbors.go:29.56,33.2 1 1
|
|
||||||
trip-planner/internal/storage/neighbors.go:36.81,43.2 1 1
|
|
||||||
trip-planner/internal/storage/neighbors.go:46.80,47.50 1 1
|
|
||||||
trip-planner/internal/storage/neighbors.go:47.50,49.3 1 1
|
|
||||||
trip-planner/internal/storage/neighbors.go:50.2,50.12 1 1
|
|
||||||
trip-planner/internal/storage/neighbors.go:54.76,55.50 1 1
|
|
||||||
trip-planner/internal/storage/neighbors.go:55.50,56.28 1 1
|
|
||||||
trip-planner/internal/storage/neighbors.go:56.28,57.43 1 1
|
|
||||||
trip-planner/internal/storage/neighbors.go:57.43,60.5 2 1
|
|
||||||
trip-planner/internal/storage/neighbors.go:66.85,67.50 1 1
|
|
||||||
trip-planner/internal/storage/neighbors.go:67.50,69.31 2 1
|
|
||||||
trip-planner/internal/storage/neighbors.go:69.31,70.21 1 1
|
|
||||||
trip-planner/internal/storage/neighbors.go:70.21,72.5 1 1
|
|
||||||
trip-planner/internal/storage/neighbors.go:74.3,74.16 1 1
|
|
||||||
trip-planner/internal/storage/neighbors.go:76.2,76.12 1 0
|
|
||||||
trip-planner/internal/storage/transfer_rules.go:14.81,16.39 1 0
|
|
||||||
trip-planner/internal/storage/transfer_rules.go:16.39,18.3 1 0
|
|
||||||
trip-planner/internal/storage/transfer_rules.go:21.2,22.39 2 0
|
|
||||||
trip-planner/internal/storage/transfer_rules.go:22.39,24.3 1 0
|
|
||||||
trip-planner/internal/storage/transfer_rules.go:27.2,27.19 1 0
|
|
||||||
trip-planner/internal/storage/transfer_rules.go:32.44,34.17 1 0
|
|
||||||
trip-planner/internal/storage/transfer_rules.go:35.63,36.28 1 0
|
|
||||||
trip-planner/internal/storage/transfer_rules.go:37.63,38.27 1 0
|
|
||||||
trip-planner/internal/storage/transfer_rules.go:39.10,40.17 1 0
|
|
||||||
trip-planner/internal/airports/airports.go:36.61,42.2 1 1
|
|
||||||
trip-planner/internal/airports/airports.go:45.65,53.2 3 1
|
|
||||||
trip-planner/internal/airports/airports.go:56.60,57.39 1 1
|
|
||||||
trip-planner/internal/airports/airports.go:57.39,59.3 1 1
|
|
||||||
trip-planner/internal/airports/airports.go:63.71,65.39 1 1
|
|
||||||
trip-planner/internal/airports/airports.go:65.39,67.3 1 1
|
|
||||||
trip-planner/internal/airports/airports.go:68.2,68.21 1 1
|
|
||||||
trip-planner/internal/airports/airports.go:72.76,73.39 1 1
|
|
||||||
trip-planner/internal/airports/airports.go:73.39,75.3 1 1
|
|
||||||
trip-planner/internal/airports/airports.go:76.2,76.19 1 1
|
|
||||||
trip-planner/internal/airports/airports.go:80.39,82.2 1 1
|
|
||||||
trip-planner/internal/airports/airports.go:85.36,86.47 1 1
|
|
||||||
trip-planner/internal/airports/airports.go:86.47,88.3 1 1
|
|
||||||
trip-planner/internal/airports/airports.go:92.64,94.33 2 1
|
|
||||||
trip-planner/internal/airports/airports.go:94.33,95.20 1 1
|
|
||||||
trip-planner/internal/airports/airports.go:95.20,97.4 1 1
|
|
||||||
trip-planner/internal/airports/airports.go:99.2,99.15 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:42.50,47.2 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:50.45,55.2 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:58.46,63.2 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:66.51,71.2 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:75.98,82.16 2 0
|
|
||||||
trip-planner/cmd/cron/station_status.go:82.16,84.3 1 0
|
|
||||||
trip-planner/cmd/cron/station_status.go:86.2,88.23 2 0
|
|
||||||
trip-planner/cmd/cron/station_status.go:95.99,104.16 7 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:104.16,106.3 1 0
|
|
||||||
trip-planner/cmd/cron/station_status.go:106.8,106.24 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:106.24,108.40 2 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:108.40,110.4 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:110.9,112.4 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:113.8,115.3 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:118.2,120.16 3 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:120.16,122.3 1 0
|
|
||||||
trip-planner/cmd/cron/station_status.go:122.8,122.32 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:122.32,125.17 3 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:125.17,127.4 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:131.2,133.40 3 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:133.40,136.17 3 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:136.17,138.4 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:138.9,140.4 1 0
|
|
||||||
trip-planner/cmd/cron/station_status.go:144.2,146.45 3 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:146.45,149.17 3 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:149.17,151.4 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:151.9,153.4 1 0
|
|
||||||
trip-planner/cmd/cron/station_status.go:157.2,159.19 2 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:159.19,164.3 4 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:164.8,166.25 2 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:166.25,168.4 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:169.3,169.20 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:169.20,171.4 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:171.9,173.4 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:177.2,177.85 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:177.85,179.3 1 0
|
|
||||||
trip-planner/cmd/cron/station_status.go:182.2,182.106 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:182.106,184.3 1 0
|
|
||||||
trip-planner/cmd/cron/station_status.go:187.2,187.115 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:187.115,189.3 1 0
|
|
||||||
trip-planner/cmd/cron/station_status.go:192.2,192.125 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:192.125,194.3 1 0
|
|
||||||
trip-planner/cmd/cron/station_status.go:196.2,196.23 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:201.73,206.33 3 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:206.33,208.3 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:208.8,210.3 1 0
|
|
||||||
trip-planner/cmd/cron/station_status.go:211.2,211.16 1 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:211.16,215.3 2 0
|
|
||||||
trip-planner/cmd/cron/station_status.go:217.2,218.16 2 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:218.16,221.3 2 0
|
|
||||||
trip-planner/cmd/cron/station_status.go:223.2,224.12 2 1
|
|
||||||
trip-planner/cmd/cron/station_status.go:230.80,231.35 1 0
|
|
||||||
trip-planner/cmd/cron/station_status.go:231.35,232.54 1 0
|
|
||||||
trip-planner/cmd/cron/station_status.go:232.54,234.4 1 0
|
|
||||||
trip-planner/cmd/cron/station_status.go:236.2,236.12 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:38.47,40.2 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:43.105,52.16 2 1
|
|
||||||
trip-planner/internal/cache/preferences.go:52.16,54.3 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:56.2,56.17 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:56.17,58.3 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:60.2,61.54 2 1
|
|
||||||
trip-planner/internal/cache/preferences.go:61.54,63.3 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:64.2,64.20 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:68.98,72.16 2 1
|
|
||||||
trip-planner/internal/cache/preferences.go:72.16,74.3 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:77.2,78.27 2 1
|
|
||||||
trip-planner/internal/cache/preferences.go:78.27,79.29 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:79.29,82.9 3 1
|
|
||||||
trip-planner/internal/cache/preferences.go:86.2,86.13 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:86.13,92.3 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:95.2,96.16 2 1
|
|
||||||
trip-planner/internal/cache/preferences.go:96.16,98.3 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:100.2,108.66 2 1
|
|
||||||
trip-planner/internal/cache/preferences.go:108.66,110.3 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:112.2,112.12 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:116.91,128.16 3 1
|
|
||||||
trip-planner/internal/cache/preferences.go:128.16,130.3 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:133.2,134.27 2 1
|
|
||||||
trip-planner/internal/cache/preferences.go:134.27,135.29 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:135.29,137.4 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:140.2,140.22 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:140.22,143.3 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:146.2,147.16 2 1
|
|
||||||
trip-planner/internal/cache/preferences.go:147.16,149.3 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:151.2,151.45 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:155.111,166.16 3 1
|
|
||||||
trip-planner/internal/cache/preferences.go:166.16,168.3 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:170.2,170.17 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:170.17,172.3 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:174.2,175.55 2 1
|
|
||||||
trip-planner/internal/cache/preferences.go:175.55,177.3 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:178.2,178.21 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:182.106,194.16 3 1
|
|
||||||
trip-planner/internal/cache/preferences.go:194.16,196.3 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:199.2,210.23 2 1
|
|
||||||
trip-planner/internal/cache/preferences.go:210.23,212.3 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:215.2,216.16 2 1
|
|
||||||
trip-planner/internal/cache/preferences.go:216.16,218.3 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:220.2,220.61 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:220.61,222.3 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:224.2,224.12 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:228.109,239.16 3 1
|
|
||||||
trip-planner/internal/cache/preferences.go:239.16,241.3 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:244.2,246.32 3 1
|
|
||||||
trip-planner/internal/cache/preferences.go:246.32,247.43 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:247.43,249.4 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:252.2,252.33 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:252.33,255.3 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:258.2,259.16 2 1
|
|
||||||
trip-planner/internal/cache/preferences.go:259.16,261.3 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:263.2,263.61 1 1
|
|
||||||
trip-planner/internal/cache/preferences.go:263.61,265.3 1 0
|
|
||||||
trip-planner/internal/cache/preferences.go:267.2,267.12 1 1
|
|
||||||
trip-planner/internal/cache/store.go:48.76,50.2 1 1
|
|
||||||
trip-planner/internal/cache/store.go:53.79,55.31 2 1
|
|
||||||
trip-planner/internal/cache/store.go:55.31,58.3 2 1
|
|
||||||
trip-planner/internal/cache/store.go:59.2,59.16 1 1
|
|
||||||
trip-planner/internal/cache/store.go:59.16,61.3 1 0
|
|
||||||
trip-planner/internal/cache/store.go:62.2,63.17 2 1
|
|
||||||
trip-planner/internal/cache/store.go:67.102,69.2 1 1
|
|
||||||
trip-planner/internal/cache/store.go:72.80,74.16 2 1
|
|
||||||
trip-planner/internal/cache/store.go:74.16,76.3 1 0
|
|
||||||
trip-planner/internal/cache/store.go:77.2,77.23 1 1
|
|
||||||
trip-planner/internal/cache/store.go:81.72,83.2 1 1
|
|
||||||
trip-planner/internal/cache/store.go:86.84,88.2 1 0
|
|
||||||
trip-planner/internal/cache/store.go:91.84,93.2 1 0
|
|
||||||
trip-planner/internal/cache/store.go:97.44,106.2 7 1
|
|
||||||
trip-planner/internal/cache/store.go:108.36,109.9 1 1
|
|
||||||
trip-planner/internal/cache/store.go:110.54,111.74 1 0
|
|
||||||
trip-planner/internal/cache/store.go:112.58,113.78 1 0
|
|
||||||
trip-planner/internal/cache/store.go:114.24,115.64 1 1
|
|
||||||
trip-planner/internal/cache/store.go:116.27,117.66 1 1
|
|
||||||
trip-planner/internal/cache/store.go:118.26,122.33 1 1
|
|
||||||
trip-planner/internal/cache/store.go:123.10,124.65 1 0
|
|
||||||
trip-planner/internal/cache/store.go:134.68,138.2 1 0
|
|
||||||
trip-planner/internal/cache/store.go:153.40,155.2 1 0
|
|
||||||
trip-planner/internal/cache/store.go:158.41,160.2 1 0
|
|
||||||
trip-planner/internal/cache/store.go:163.52,165.2 1 0
|
|
||||||
trip-planner/internal/cache/store.go:175.65,177.2 1 1
|
|
||||||
trip-planner/internal/cache/store.go:182.143,184.67 1 1
|
|
||||||
trip-planner/internal/cache/store.go:184.67,186.3 1 1
|
|
||||||
trip-planner/internal/cache/store.go:189.2,190.16 2 1
|
|
||||||
trip-planner/internal/cache/store.go:190.16,192.3 1 0
|
|
||||||
trip-planner/internal/cache/store.go:195.2,195.57 1 1
|
|
||||||
trip-planner/internal/cache/store.go:195.57,197.3 1 0
|
|
||||||
trip-planner/internal/cache/store.go:199.2,199.18 1 1
|
|
||||||
trip-planner/internal/cache/store.go:203.112,205.2 1 1
|
|
||||||
trip-planner/internal/cache/store.go:208.115,210.2 1 0
|
|
||||||
trip-planner/internal/cache/store.go:215.130,217.15 2 1
|
|
||||||
trip-planner/internal/cache/store.go:217.15,219.3 1 1
|
|
||||||
trip-planner/internal/cache/store.go:219.8,221.3 1 1
|
|
||||||
trip-planner/internal/cache/store.go:224.2,225.16 2 1
|
|
||||||
trip-planner/internal/cache/store.go:225.16,227.3 1 0
|
|
||||||
trip-planner/internal/cache/store.go:228.2,228.17 1 1
|
|
||||||
trip-planner/internal/cache/store.go:228.17,231.3 2 0
|
|
||||||
trip-planner/internal/cache/store.go:234.2,238.16 3 1
|
|
||||||
trip-planner/internal/cache/store.go:238.16,240.3 1 0
|
|
||||||
trip-planner/internal/cache/store.go:243.2,243.57 1 1
|
|
||||||
trip-planner/internal/cache/store.go:243.57,245.3 1 0
|
|
||||||
trip-planner/internal/cache/store.go:247.2,247.18 1 1
|
|
||||||
trip-planner/internal/cache/store.go:251.79,253.2 1 1
|
|
||||||
trip-planner/internal/cache/store.go:256.82,258.2 1 1
|
|
||||||
trip-planner/internal/cache/store.go:261.81,263.2 1 1
|
|
||||||
trip-planner/internal/cache/store.go:266.71,268.2 1 0
|
|
||||||
trip-planner/internal/cache/store.go:271.78,273.16 2 0
|
|
||||||
trip-planner/internal/cache/store.go:273.16,275.3 1 0
|
|
||||||
trip-planner/internal/cache/store.go:276.2,276.16 1 0
|
|
||||||
trip-planner/internal/cache/store.go:276.16,279.3 2 0
|
|
||||||
trip-planner/internal/cache/store.go:280.2,281.17 2 0
|
|
||||||
trip-planner/internal/cache/store.go:285.101,287.2 1 0
|
|
||||||
trip-planner/internal/cache/store.go:290.79,292.16 2 0
|
|
||||||
trip-planner/internal/cache/store.go:292.16,294.3 1 0
|
|
||||||
trip-planner/internal/cache/store.go:295.2,295.20 1 0
|
|
||||||
trip-planner/internal/cache/store.go:299.83,301.2 1 0
|
|
||||||
trip-planner/internal/cache/store.go:304.83,306.2 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:100.24,106.2 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:109.37,111.2 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:114.37,116.2 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:119.33,123.2 3 1
|
|
||||||
trip-planner/internal/routing/graph.go:126.33,130.2 3 0
|
|
||||||
trip-planner/internal/routing/graph.go:135.60,142.30 3 0
|
|
||||||
trip-planner/internal/routing/graph.go:142.30,154.51 4 0
|
|
||||||
trip-planner/internal/routing/graph.go:154.51,162.4 3 0
|
|
||||||
trip-planner/internal/routing/graph.go:165.3,167.33 3 0
|
|
||||||
trip-planner/internal/routing/graph.go:167.33,169.4 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:169.9,169.36 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:169.36,171.4 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:172.3,193.5 2 0
|
|
||||||
trip-planner/internal/routing/graph.go:196.2,196.14 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:202.57,204.34 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:204.34,206.3 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:208.2,208.34 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:208.34,209.60 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:209.60,212.36 2 0
|
|
||||||
trip-planner/internal/routing/graph.go:212.36,214.5 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:214.10,214.39 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:214.39,216.5 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:219.4,220.36 2 0
|
|
||||||
trip-planner/internal/routing/graph.go:220.36,222.5 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:222.10,224.5 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:227.4,248.6 2 0
|
|
||||||
trip-planner/internal/routing/graph.go:254.31,255.40 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:255.40,257.3 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:261.57,263.31 2 1
|
|
||||||
trip-planner/internal/routing/graph.go:263.31,265.3 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:266.2,266.12 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:270.44,271.28 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:271.28,272.17 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:272.17,274.4 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:276.2,276.12 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:283.190,288.56 2 1
|
|
||||||
trip-planner/internal/routing/graph.go:288.56,291.31 2 0
|
|
||||||
trip-planner/internal/routing/graph.go:291.31,293.4 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:294.3,294.29 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:294.29,296.4 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:299.3,299.54 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:299.54,302.21 2 0
|
|
||||||
trip-planner/internal/routing/graph.go:302.21,304.25 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:304.25,306.6 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:308.5,308.32 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:308.32,310.6 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:313.4,313.46 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:313.46,316.34 2 0
|
|
||||||
trip-planner/internal/routing/graph.go:316.34,317.152 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:317.152,319.12 2 0
|
|
||||||
trip-planner/internal/routing/graph.go:322.5,322.23 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:322.23,343.6 2 0
|
|
||||||
trip-planner/internal/routing/graph.go:349.2,356.41 4 1
|
|
||||||
trip-planner/internal/routing/graph.go:356.41,358.3 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:361.2,387.21 7 1
|
|
||||||
trip-planner/internal/routing/graph.go:387.21,393.31 3 1
|
|
||||||
trip-planner/internal/routing/graph.go:393.31,395.89 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:395.89,400.5 3 1
|
|
||||||
trip-planner/internal/routing/graph.go:402.4,402.12 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:406.3,406.44 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:406.44,414.33 4 1
|
|
||||||
trip-planner/internal/routing/graph.go:414.33,417.5 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:419.4,423.52 3 1
|
|
||||||
trip-planner/internal/routing/graph.go:423.52,424.48 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:424.48,426.14 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:429.4,432.23 3 1
|
|
||||||
trip-planner/internal/routing/graph.go:432.23,434.5 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:437.4,441.40 3 1
|
|
||||||
trip-planner/internal/routing/graph.go:441.40,449.5 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:449.10,457.5 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:459.4,467.66 2 1
|
|
||||||
trip-planner/internal/routing/graph.go:467.66,468.13 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:471.4,477.6 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:481.3,481.41 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:481.41,482.46 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:482.46,484.5 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:485.4,485.50 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:491.2,491.17 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:491.17,495.24 2 1
|
|
||||||
trip-planner/internal/routing/graph.go:495.24,517.24 8 1
|
|
||||||
trip-planner/internal/routing/graph.go:517.24,521.33 3 1
|
|
||||||
trip-planner/internal/routing/graph.go:521.33,523.93 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:523.93,527.7 3 0
|
|
||||||
trip-planner/internal/routing/graph.go:528.6,528.14 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:531.5,531.72 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:531.72,532.14 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:535.5,535.46 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:535.46,541.35 4 1
|
|
||||||
trip-planner/internal/routing/graph.go:541.35,543.7 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:545.6,549.54 3 1
|
|
||||||
trip-planner/internal/routing/graph.go:549.54,550.50 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:550.50,552.16 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:555.6,558.25 3 1
|
|
||||||
trip-planner/internal/routing/graph.go:558.25,560.7 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:562.6,565.42 3 1
|
|
||||||
trip-planner/internal/routing/graph.go:565.42,573.7 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:573.12,581.7 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:583.6,590.68 2 1
|
|
||||||
trip-planner/internal/routing/graph.go:590.68,591.15 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:594.6,600.8 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:604.5,604.44 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:604.44,605.50 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:605.50,607.7 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:608.6,608.54 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:612.4,612.20 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:612.20,614.5 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:619.3,619.44 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:619.44,630.18 4 0
|
|
||||||
trip-planner/internal/routing/graph.go:630.18,633.5 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:636.4,636.38 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:636.38,641.24 3 0
|
|
||||||
trip-planner/internal/routing/graph.go:641.24,648.6 2 0
|
|
||||||
trip-planner/internal/routing/graph.go:649.5,649.22 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:649.22,656.6 2 0
|
|
||||||
trip-planner/internal/routing/graph.go:658.5,666.7 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:670.4,688.24 7 0
|
|
||||||
trip-planner/internal/routing/graph.go:688.24,692.33 3 0
|
|
||||||
trip-planner/internal/routing/graph.go:692.33,694.93 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:694.93,698.7 3 0
|
|
||||||
trip-planner/internal/routing/graph.go:699.6,699.14 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:702.5,702.72 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:702.72,703.14 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:706.5,706.46 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:706.46,712.35 4 0
|
|
||||||
trip-planner/internal/routing/graph.go:712.35,714.7 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:716.6,720.54 3 0
|
|
||||||
trip-planner/internal/routing/graph.go:720.54,721.50 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:721.50,723.16 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:726.6,729.25 3 0
|
|
||||||
trip-planner/internal/routing/graph.go:729.25,731.7 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:733.6,736.42 3 0
|
|
||||||
trip-planner/internal/routing/graph.go:736.42,744.7 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:744.12,752.7 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:754.6,761.68 2 0
|
|
||||||
trip-planner/internal/routing/graph.go:761.68,762.15 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:765.6,771.8 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:775.5,775.44 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:775.44,776.50 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:776.50,778.7 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:779.6,779.54 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:783.4,783.20 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:783.20,785.5 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:788.3,788.13 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:791.2,791.13 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:796.72,797.50 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:797.50,800.3 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:803.2,803.18 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:803.18,805.3 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:808.2,811.41 3 0
|
|
||||||
trip-planner/internal/routing/graph.go:811.41,821.41 5 0
|
|
||||||
trip-planner/internal/routing/graph.go:821.41,823.4 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:826.3,826.45 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:826.45,828.4 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:831.3,831.33 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:835.2,836.18 2 0
|
|
||||||
trip-planner/internal/routing/graph.go:892.172,897.75 2 1
|
|
||||||
trip-planner/internal/routing/graph.go:897.75,902.48 4 1
|
|
||||||
trip-planner/internal/routing/graph.go:902.48,904.4 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:908.2,908.26 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:909.26,910.50 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:910.50,911.76 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:911.76,913.5 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:914.4,914.74 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:914.74,916.5 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:917.4,917.58 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:919.18,920.50 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:920.50,921.56 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:921.56,923.5 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:924.4,924.74 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:924.74,926.5 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:927.4,927.78 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:929.10,930.50 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:930.50,931.74 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:931.74,933.5 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:934.4,934.76 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:934.76,936.5 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:937.4,937.58 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:943.2,944.43 2 1
|
|
||||||
trip-planner/internal/routing/graph.go:944.43,946.35 2 1
|
|
||||||
trip-planner/internal/routing/graph.go:946.35,953.38 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:953.38,955.10 2 0
|
|
||||||
trip-planner/internal/routing/graph.go:958.3,958.17 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:958.17,960.4 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:963.2,963.15 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:979.51,984.17 2 1
|
|
||||||
trip-planner/internal/routing/graph.go:984.17,986.3 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:993.2,993.19 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:999.80,1003.30 2 0
|
|
||||||
trip-planner/internal/routing/graph.go:1003.30,1014.24 2 0
|
|
||||||
trip-planner/internal/routing/graph.go:1014.24,1016.4 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:1019.2,1019.13 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:1047.65,1051.67 2 1
|
|
||||||
trip-planner/internal/routing/graph.go:1051.67,1053.3 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:1055.2,1059.37 3 1
|
|
||||||
trip-planner/internal/routing/graph.go:1059.37,1062.32 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:1062.32,1063.62 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:1063.62,1066.30 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:1066.30,1070.11 4 1
|
|
||||||
trip-planner/internal/routing/graph.go:1073.5,1073.59 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:1073.59,1074.74 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:1074.74,1078.7 3 0
|
|
||||||
trip-planner/internal/routing/graph.go:1082.3,1082.20 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:1082.20,1083.9 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:1087.2,1087.22 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:1092.89,1095.19 2 1
|
|
||||||
trip-planner/internal/routing/graph.go:1095.19,1099.3 3 1
|
|
||||||
trip-planner/internal/routing/graph.go:1100.2,1100.15 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:1105.119,1106.39 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:1106.39,1108.3 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:1109.2,1109.18 1 1
|
|
||||||
trip-planner/internal/routing/graph.go:1113.80,1116.17 2 0
|
|
||||||
trip-planner/internal/routing/graph.go:1116.17,1118.3 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:1119.2,1124.30 3 0
|
|
||||||
trip-planner/internal/routing/graph.go:1124.30,1125.79 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:1125.79,1133.4 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:1136.2,1136.25 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:1136.25,1138.3 1 0
|
|
||||||
trip-planner/internal/routing/graph.go:1140.2,1140.18 1 0
|
|
||||||
trip-planner/internal/routing/search_cache.go:21.116,27.2 1 1
|
|
||||||
trip-planner/internal/routing/search_cache.go:31.136,36.38 2 0
|
|
||||||
trip-planner/internal/routing/search_cache.go:36.38,39.3 1 0
|
|
||||||
trip-planner/internal/routing/search_cache.go:42.2,44.16 3 0
|
|
||||||
trip-planner/internal/routing/search_cache.go:44.16,46.3 1 0
|
|
||||||
trip-planner/internal/routing/search_cache.go:49.2,50.54 2 0
|
|
||||||
trip-planner/internal/routing/search_cache.go:50.54,52.3 1 0
|
|
||||||
trip-planner/internal/routing/search_cache.go:54.2,54.21 1 0
|
|
||||||
trip-planner/internal/routing/search_cache.go:58.130,68.16 3 0
|
|
||||||
trip-planner/internal/routing/search_cache.go:68.16,70.3 1 0
|
|
||||||
trip-planner/internal/routing/search_cache.go:73.2,73.37 1 0
|
|
||||||
trip-planner/internal/routing/search_cache.go:77.68,78.17 1 1
|
|
||||||
trip-planner/internal/routing/search_cache.go:78.17,80.3 1 1
|
|
||||||
trip-planner/internal/routing/search_cache.go:81.2,82.16 2 1
|
|
||||||
trip-planner/internal/routing/search_cache.go:82.16,84.3 1 0
|
|
||||||
trip-planner/internal/routing/search_cache.go:85.2,85.18 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:66.83,68.17 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:68.17,71.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:74.2,76.33 3 1
|
|
||||||
trip-planner/cmd/api/handlers.go:99.79,101.20 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:101.20,104.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:105.2,114.35 4 1
|
|
||||||
trip-planner/cmd/api/handlers.go:114.35,117.42 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:117.42,118.62 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:118.62,119.42 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:119.42,121.11 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:125.3,125.20 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:125.20,127.4 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:131.2,132.35 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:132.35,136.20 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:136.20,139.4 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:140.3,140.20 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:140.20,142.4 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:145.3,146.35 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:146.35,154.4 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:157.2,162.33 3 1
|
|
||||||
trip-planner/cmd/api/handlers.go:167.55,168.16 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:169.11,170.52 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:171.11,172.52 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:173.10,174.38 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:179.78,185.61 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:185.61,188.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:191.2,214.50 7 1
|
|
||||||
trip-planner/cmd/api/handlers.go:214.50,217.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:218.2,218.50 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:218.50,220.3 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:223.2,223.64 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:223.64,225.35 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:225.35,227.4 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:231.2,239.32 5 1
|
|
||||||
trip-planner/cmd/api/handlers.go:239.32,248.3 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:250.2,255.33 3 1
|
|
||||||
trip-planner/cmd/api/handlers.go:259.79,261.20 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:261.20,264.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:269.2,274.41 3 1
|
|
||||||
trip-planner/cmd/api/handlers.go:274.41,275.22 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:275.22,278.4 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:283.2,285.41 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:285.41,290.21 3 1
|
|
||||||
trip-planner/cmd/api/handlers.go:290.21,292.4 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:295.3,295.29 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:296.35,297.27 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:298.33,299.27 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:300.35,301.27 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:306.3,310.22 4 1
|
|
||||||
trip-planner/cmd/api/handlers.go:310.22,311.12 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:313.3,341.53 4 1
|
|
||||||
trip-planner/cmd/api/handlers.go:341.53,366.4 3 1
|
|
||||||
trip-planner/cmd/api/handlers.go:369.2,375.56 3 1
|
|
||||||
trip-planner/cmd/api/handlers.go:375.56,378.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:382.80,386.20 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:386.20,389.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:390.2,397.20 4 1
|
|
||||||
trip-planner/cmd/api/handlers.go:397.20,398.42 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:398.42,399.60 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:399.60,400.42 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:400.42,402.11 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:408.2,409.19 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:409.19,411.3 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:413.2,414.20 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:414.20,416.3 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:418.2,426.33 3 1
|
|
||||||
trip-planner/cmd/api/handlers.go:431.81,434.26 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:434.26,438.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:439.2,440.65 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:440.65,443.3 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:444.2,444.13 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:449.85,451.26 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:451.26,453.3 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:456.2,458.20 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:458.20,461.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:462.2,469.61 3 1
|
|
||||||
trip-planner/cmd/api/handlers.go:469.61,472.3 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:475.2,479.32 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:479.32,482.3 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:485.2,485.28 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:485.28,488.3 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:492.2,502.33 4 1
|
|
||||||
trip-planner/cmd/api/handlers.go:507.58,514.2 3 1
|
|
||||||
trip-planner/cmd/api/handlers.go:524.81,526.18 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:526.18,528.3 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:530.2,531.16 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:531.16,534.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:536.2,540.4 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:544.79,546.18 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:546.18,548.3 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:550.2,554.61 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:554.61,557.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:559.2,559.97 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:559.97,562.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:564.2,567.4 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:571.82,573.18 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:573.18,575.3 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:577.2,579.20 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:579.20,582.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:583.2,585.86 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:585.86,588.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:590.2,593.4 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:597.83,599.18 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:599.18,601.3 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:603.2,604.16 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:604.16,607.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:609.2,613.4 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:617.83,619.18 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:619.18,621.3 1 0
|
|
||||||
trip-planner/cmd/api/handlers.go:623.2,628.61 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:628.61,631.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:634.2,634.76 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:634.76,637.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:639.2,639.113 1 1
|
|
||||||
trip-planner/cmd/api/handlers.go:639.113,642.3 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:644.2,647.4 2 1
|
|
||||||
trip-planner/cmd/api/handlers.go:651.81,654.2 2 0
|
|
||||||
trip-planner/cmd/api/handlers.go:657.133,669.2 2 1
|
|
||||||
trip-planner/cmd/api/main.go:17.13,23.18 5 0
|
|
||||||
trip-planner/cmd/api/main.go:23.18,25.3 1 0
|
|
||||||
trip-planner/cmd/api/main.go:26.2,43.46 12 0
|
|
||||||
trip-planner/cmd/api/main.go:47.32,54.2 2 0
|
|
||||||
trip-planner/cmd/api/main.go:58.73,63.2 2 0
|
|
||||||
trip-planner/cmd/api/main.go:67.122,68.54 1 0
|
|
||||||
trip-planner/cmd/api/main.go:68.54,70.3 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:67.58,83.30 2 1
|
|
||||||
trip-planner/internal/yandex/client.go:83.30,85.3 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:87.2,87.10 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:94.55,95.25 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:95.25,97.3 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:101.62,102.25 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:102.25,104.3 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:108.97,109.25 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:109.25,116.3 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:120.45,121.25 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:121.25,123.3 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:127.107,129.48 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:129.48,131.3 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:134.2,140.67 4 0
|
|
||||||
trip-planner/internal/yandex/client.go:140.67,142.32 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:142.32,145.4 2 0
|
|
||||||
trip-planner/internal/yandex/client.go:147.3,148.17 2 0
|
|
||||||
trip-planner/internal/yandex/client.go:148.17,151.4 2 0
|
|
||||||
trip-planner/internal/yandex/client.go:154.3,154.29 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:154.29,157.4 2 0
|
|
||||||
trip-planner/internal/yandex/client.go:159.3,161.41 2 0
|
|
||||||
trip-planner/internal/yandex/client.go:161.41,163.28 2 0
|
|
||||||
trip-planner/internal/yandex/client.go:163.28,165.5 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:166.4,166.23 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:170.2,172.17 3 0
|
|
||||||
trip-planner/internal/yandex/client.go:176.85,178.16 2 0
|
|
||||||
trip-planner/internal/yandex/client.go:178.16,180.3 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:182.2,185.20 2 0
|
|
||||||
trip-planner/internal/yandex/client.go:185.20,187.3 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:189.2,190.16 2 0
|
|
||||||
trip-planner/internal/yandex/client.go:190.16,192.3 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:194.2,194.28 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:194.28,198.3 3 0
|
|
||||||
trip-planner/internal/yandex/client.go:200.2,201.65 2 0
|
|
||||||
trip-planner/internal/yandex/client.go:201.65,203.3 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:205.2,205.19 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:262.35,264.2 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:266.54,268.2 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:271.39,272.16 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:272.16,274.3 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:276.2,277.8 2 1
|
|
||||||
trip-planner/internal/yandex/client.go:277.8,279.3 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:281.2,285.56 2 1
|
|
||||||
trip-planner/internal/yandex/client.go:289.60,292.26 3 1
|
|
||||||
trip-planner/internal/yandex/client.go:292.26,294.3 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:295.2,296.10 2 1
|
|
||||||
trip-planner/internal/yandex/client.go:301.60,308.2 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:310.40,317.19 5 1
|
|
||||||
trip-planner/internal/yandex/client.go:317.19,320.3 2 1
|
|
||||||
trip-planner/internal/yandex/client.go:322.2,322.117 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:325.46,327.28 2 1
|
|
||||||
trip-planner/internal/yandex/client.go:327.28,329.42 2 1
|
|
||||||
trip-planner/internal/yandex/client.go:329.42,331.4 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:331.9,333.4 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:334.3,334.22 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:341.42,347.2 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:349.49,356.2 6 1
|
|
||||||
trip-planner/internal/yandex/client.go:358.40,362.18 3 1
|
|
||||||
trip-planner/internal/yandex/client.go:363.14,364.14 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:365.12,367.45 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:367.45,371.4 3 1
|
|
||||||
trip-planner/internal/yandex/client.go:372.3,372.15 1 1
|
|
||||||
trip-planner/internal/yandex/client.go:373.16,374.14 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:376.2,376.14 1 0
|
|
||||||
trip-planner/internal/yandex/client.go:379.43,383.18 3 1
|
|
||||||
trip-planner/internal/yandex/client.go:384.14,384.14 0 0
|
|
||||||
trip-planner/internal/yandex/client.go:386.16,388.24 2 1
|
|
||||||
trip-planner/internal/yandex/client.go:388.24,391.4 2 1
|
|
||||||
trip-planner/internal/yandex/client.go:392.12,392.12 0 0
|
|
||||||
trip-planner/internal/yandex/client.go:397.43,401.18 3 1
|
|
||||||
trip-planner/internal/yandex/client.go:402.14,404.38 2 1
|
|
||||||
trip-planner/internal/yandex/client.go:404.38,407.4 2 1
|
|
||||||
trip-planner/internal/yandex/client.go:408.16,410.28 2 0
|
|
||||||
trip-planner/internal/yandex/client.go:411.12,411.12 0 1
|
|
||||||
trip-planner/internal/yandex/client.go:418.55,421.2 2 0
|
|
||||||
trip-planner/internal/yandex/client.go:423.28,426.2 1 0
|
|
||||||
@@ -16,10 +16,25 @@ services:
|
|||||||
- postgres
|
- postgres
|
||||||
command: ["/api"]
|
command: ["/api"]
|
||||||
|
|
||||||
|
cron:
|
||||||
|
build: .
|
||||||
|
environment:
|
||||||
|
- REDIS_ADDR=redis:6379
|
||||||
|
- POSTGRES_DATABASE=trip_planner
|
||||||
|
- POSTGRES_USER=trip_planner
|
||||||
|
- POSTGRES_PASSWORD=trip_planner
|
||||||
|
- POSTGRES_HOST=postgres
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
- postgres
|
||||||
|
command: ["/cron"]
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
@@ -32,5 +47,12 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
watchtower:
|
||||||
|
image: nickfedor/watchtower:latest
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
command: --interval 60 --cleanup travel-api
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# 2026-08-18-implement-deployment-infrastructure
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
- Clear description of the feature/change being implemented: Implement the complete deployment infrastructure for the trip-planner service including Dockerfile, updated docker-compose.yml with cron and watchtower services, and Gitea Actions CI/CD workflow.
|
||||||
|
- Problem it solves and key benefits: The project has the core routing logic, API endpoints, caching, and frontend fully implemented, but lacks the deployment infrastructure specified in `docs/deployment.md`. This plan adds the missing Dockerfile, completes docker-compose.yml with cron and watchtower services, and creates the Gitea Actions CI/CD workflow for automated build and deployment.
|
||||||
|
- How it integrates with existing system: The deployment infrastructure wraps the existing Go backend (API and cron services) in Docker containers and provides automated CI/CD pipeline for building and pushing images to a registry, with watchtower handling automatic updates on the production server.
|
||||||
|
|
||||||
|
## Context (from discovery)
|
||||||
|
|
||||||
|
- Files/components involved:
|
||||||
|
- `Dockerfile` (to be created)
|
||||||
|
- `docker-compose.yml` (to be updated with cron and watchtower services, redis_data volume)
|
||||||
|
- `.gitea/workflows/deploy.yml` (to be created)
|
||||||
|
- Existing: `/cmd/api/main.go`, `/cmd/cron/station_status.go`, `go.mod`, `go.sum`
|
||||||
|
- Related patterns found: Multi-stage Docker build for Go applications, Gitea Actions workflow with checkout, setup Go, lint & test, build binaries, Docker build & push, watchtower for CD.
|
||||||
|
- Dependencies identified: Go 1.22+, PostgreSQL 15-alpine, Redis 7-alpine, nickfedor/watchtower image.
|
||||||
|
|
||||||
|
## Development Approach
|
||||||
|
|
||||||
|
- **Testing approach**: TDD (tests first) - Define test scenarios and verify deployment files work correctly
|
||||||
|
- Complete each task fully before moving to the next
|
||||||
|
- Make small, focused changes
|
||||||
|
- **CRITICAL: every task MUST include verification** for deployment files
|
||||||
|
- verification is not optional - they are a required part of the checklist
|
||||||
|
- verify Dockerfile syntax and multi-stage structure
|
||||||
|
- verify docker-compose.yml syntax and service definitions
|
||||||
|
- verify Gitea Actions workflow YAML syntax
|
||||||
|
- include both success and error scenarios in verification
|
||||||
|
- **CRITICAL: all verifications must pass before starting next task** - no exceptions
|
||||||
|
- **CRITICAL: update this plan file when scope changes during implementation**
|
||||||
|
- Keep plan in sync with actual work done
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
- **Verification tests**: required for every task (see Development Approach above)
|
||||||
|
- **Syntax validation**: verify YAML files with yaml lint, verify Dockerfile with docker build --dry-run
|
||||||
|
- **Configuration validation**: verify docker-compose.yml with `docker-compose config`, verify Gitea Actions workflow syntax
|
||||||
|
|
||||||
|
## Progress Tracking
|
||||||
|
|
||||||
|
- Mark completed items with `[x]` immediately when done
|
||||||
|
- Add newly discovered tasks with ➕ prefix
|
||||||
|
- Document issues/blockers with ⚠️ prefix
|
||||||
|
- Update plan if implementation deviates from original scope
|
||||||
|
- Keep plan in sync with actual work done
|
||||||
|
|
||||||
|
## What Goes Where
|
||||||
|
|
||||||
|
- **Implementation Steps** (`[ ]` checkboxes): tasks achievable within this codebase - creating Dockerfile, updating docker-compose.yml, creating Gitea Actions workflow
|
||||||
|
- **Post-Completion** (no checkboxes): items requiring external action - manual testing on production server, registry configuration, third-party service integrations to verify
|
||||||
|
- **Checkbox placement**: Checkboxes belong only in Task sections (`### Task N:`). Do not put checkboxes in Success criteria, Overview, or Context — they cause extra loop iterations.
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
### Task 1: Create Multi-stage Dockerfile
|
||||||
|
- [x] create Dockerfile with builder stage (Go 1.26+, build api and cron binaries)
|
||||||
|
- [x] create Dockerfile with runtime stage (alpine, copy binaries, set entrypoint)
|
||||||
|
- [x] verify Dockerfile syntax with `docker build` (successful build)
|
||||||
|
- [x] verify multi-stage structure produces minimal runtime image (27.1MB)
|
||||||
|
|
||||||
|
### Task 2: Update docker-compose.yml
|
||||||
|
- [x] add `cron` service (Go cron binary, depends_on: postgres, redis)
|
||||||
|
- [x] add `watchtower` service (nickfedor/watchtower image, volume for docker.sock, command for cleanup)
|
||||||
|
- [x] add `redis_data` volume to volumes section
|
||||||
|
- [x] verify docker-compose.yml syntax with `docker-compose config`
|
||||||
|
|
||||||
|
### Task 3: Create Gitea Actions CI/CD Workflow
|
||||||
|
- [x] create `.gitea/workflows/deploy.yml` file
|
||||||
|
- [x] add checkout step (actions/checkout@v3)
|
||||||
|
- [x] add setup Go step (actions/setup-go@v4, go-version: '1.22')
|
||||||
|
- [x] add Go modules cache step (actions/cache@v3)
|
||||||
|
- [x] add lint & test step (golangci-lint, go test -v -race ./...)
|
||||||
|
- [x] add Docker Buildx setup step (docker/setup-buildx-action@v2)
|
||||||
|
- [x] add Docker login step (docker/login-action@v2)
|
||||||
|
- [x] add Docker build & push step (docker/build-push-action@v4, tags: latest and {{.CommitID}})
|
||||||
|
- [x] verify Gitea Actions workflow YAML syntax
|
||||||
|
|
||||||
|
### Task 4: Verify acceptance criteria
|
||||||
|
- [x] verify Dockerfile matches multi-stage specification
|
||||||
|
- [x] verify docker-compose.yml has api, cron, postgres, redis, watchtower services
|
||||||
|
- [x] verify Gitea Actions workflow has all required steps (checkout, setup Go, lint & test, build binaries, Docker build & push)
|
||||||
|
- [x] verify all YAML files are valid syntax
|
||||||
|
- [x] verify docker-compose.yml volumes section includes postgres_data and redis_data
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
- **Dockerfile structure**:
|
||||||
|
- Builder stage: `golang:1.22-alpine` as builder, copy go.mod/go.sum, `go mod download`, copy source, `go build -o api cmd/api/main.go`, `go build -o cron cmd/cron/main.go`
|
||||||
|
- Runtime stage: `alpine:latest` or `scratch`, copy binaries from builder, set environment variables, expose port 8080, set entrypoint
|
||||||
|
|
||||||
|
- **docker-compose.yml services**:
|
||||||
|
- `api`: build from Dockerfile, ports: 8080:8080, environment: DB_DSN, REDIS_ADDR, YANDEX_API_KEY, depends_on: postgres, redis
|
||||||
|
- `cron`: build from Dockerfile, environment: same as api, depends_on: postgres, redis
|
||||||
|
- `postgres` (postgres): postgres:15-alpine, environment: POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, volumes: postgres_data
|
||||||
|
- `redis`: redis:7-alpine, volumes: redis_data
|
||||||
|
- `watchtower`: nickfedor/watchtower, volumes: /var/run/docker.sock:/var/run/docker.sock, command: --interval 60 --cleanup travel-api
|
||||||
|
|
||||||
|
- **.gitea/workflows/deploy.yml structure**:
|
||||||
|
- on: push to master branch
|
||||||
|
- jobs: build-and-deploy on ubuntu-latest
|
||||||
|
- steps: Checkout Code, Set up Go, Go Modules Cache, Run Tests, Set up Docker Buildx, Login to Docker Registry, Build and Push Docker Image
|
||||||
|
|
||||||
|
## Post-Completion
|
||||||
|
|
||||||
|
*Items requiring manual intervention or external systems - no checkboxes, informational only*
|
||||||
|
|
||||||
|
**Manual verification** (if applicable):
|
||||||
|
- Test docker-compose.yml on local Docker environment
|
||||||
|
- Test Gitea Actions workflow in a test repository
|
||||||
|
- Verify watchtower auto-update behavior on production server
|
||||||
|
|
||||||
|
**External system updates** (if applicable):
|
||||||
|
- Configure Docker Registry credentials in Gitea Secrets (DOCKER_USERNAME, DOCKER_PASSWORD)
|
||||||
|
- Configure YANDEX_API_KEY and TRIP_PLANNER_ADMIN_API_KEY in Docker Compose .env file
|
||||||
|
- Verify PostgreSQL and Redis persistence volumes are properly mounted
|
||||||
@@ -557,21 +557,22 @@ func (g *Graph) FindRoute(originID, destID string, opts SearchOptions, closedSta
|
|||||||
|
|
||||||
newDurationWithMCT := newDuration + transferTime
|
newDurationWithMCT := newDuration + transferTime
|
||||||
|
|
||||||
// Check if we've visited this node with fewer transfers
|
// Calculate new transfers before checking visited
|
||||||
visKey := nextNode.ID
|
|
||||||
if existingTransfers, ok := visited[visKey]; ok {
|
|
||||||
if current.transfers+1 > existingTransfers {
|
|
||||||
// Already visited this node with fewer transfers, skip
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visited[visKey] = current.transfers + 1
|
|
||||||
|
|
||||||
newTransfers := current.transfers
|
newTransfers := current.transfers
|
||||||
if edge.IsTransfer {
|
if edge.IsTransfer {
|
||||||
newTransfers++
|
newTransfers++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we've visited this node with fewer transfers
|
||||||
|
visKey := nextNode.ID
|
||||||
|
if existingTransfers, ok := visited[visKey]; ok {
|
||||||
|
if newTransfers > existingTransfers {
|
||||||
|
// Already visited this node with fewer transfers, skip
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visited[visKey] = newTransfers
|
||||||
|
|
||||||
newLegs := make([]RouteLeg, len(current.itinerary.Legs)+1)
|
newLegs := make([]RouteLeg, len(current.itinerary.Legs)+1)
|
||||||
copy(newLegs, current.itinerary.Legs)
|
copy(newLegs, current.itinerary.Legs)
|
||||||
|
|
||||||
@@ -733,21 +734,22 @@ func (g *Graph) FindRoute(originID, destID string, opts SearchOptions, closedSta
|
|||||||
|
|
||||||
newDurationWithMCT := newDuration + transferTime
|
newDurationWithMCT := newDuration + transferTime
|
||||||
|
|
||||||
// Check if we've visited this node with fewer transfers
|
// Calculate new transfers before checking visited
|
||||||
visKey := nextNode.ID
|
|
||||||
if existingTransfers, ok := visited[visKey]; ok {
|
|
||||||
if current.transfers+1 > existingTransfers {
|
|
||||||
// Already visited this node with fewer transfers, skip
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visited[visKey] = current.transfers + 1
|
|
||||||
|
|
||||||
newTransfers := current.transfers
|
newTransfers := current.transfers
|
||||||
if edge.IsTransfer {
|
if edge.IsTransfer {
|
||||||
newTransfers++
|
newTransfers++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we've visited this node with fewer transfers
|
||||||
|
visKey := nextNode.ID
|
||||||
|
if existingTransfers, ok := visited[visKey]; ok {
|
||||||
|
if newTransfers > existingTransfers {
|
||||||
|
// Already visited this node with fewer transfers, skip
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visited[visKey] = newTransfers
|
||||||
|
|
||||||
newLegs := make([]RouteLeg, len(current.itinerary.Legs)+1)
|
newLegs := make([]RouteLeg, len(current.itinerary.Legs)+1)
|
||||||
copy(newLegs, current.itinerary.Legs)
|
copy(newLegs, current.itinerary.Legs)
|
||||||
|
|
||||||
@@ -1106,7 +1108,7 @@ func (g *Graph) checkRouteForChanges(itinerary *Itinerary) bool {
|
|||||||
}
|
}
|
||||||
// Check for significant delay (more than 2x normal duration)
|
// Check for significant delay (more than 2x normal duration)
|
||||||
if edge.Duration > leg.Duration*2 && leg.Duration > 0 {
|
if edge.Duration > leg.Duration*2 && leg.Duration > 0 {
|
||||||
if !needsReSearch || itinerary.ReSearchReason == string(reasonNone) {
|
if !needsReSearch {
|
||||||
needsReSearch = true
|
needsReSearch = true
|
||||||
itinerary.NeedsReSearch = true
|
itinerary.NeedsReSearch = true
|
||||||
itinerary.ReSearchReason = string(reasonMajorDelay)
|
itinerary.ReSearchReason = string(reasonMajorDelay)
|
||||||
@@ -1129,8 +1131,8 @@ func (g *Graph) rescheduleRoute(originID, destID string, opts SearchOptions, clo
|
|||||||
result := g.FindRoute(originID, destID, opts, closedStations, neighbors, yclient...)
|
result := g.FindRoute(originID, destID, opts, closedStations, neighbors, yclient...)
|
||||||
if result != nil {
|
if result != nil {
|
||||||
result.LastChecked = time.Now().Unix()
|
result.LastChecked = time.Now().Unix()
|
||||||
result.NeedsReSearch = false
|
// Keep NeedsReSearch and ReSearchReason from the original itinerary to indicate
|
||||||
result.ReSearchReason = string(reasonNone)
|
// that a re-search was triggered due to changes
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -1139,7 +1141,13 @@ func (g *Graph) rescheduleRoute(originID, destID string, opts SearchOptions, clo
|
|||||||
// This is the main entry point for flight change notification logic.
|
// This is the main entry point for flight change notification logic.
|
||||||
func (g *Graph) CheckAndRescheduleRoute(itinerary *Itinerary, originID, destID string, opts SearchOptions, closedStations map[string]bool, neighbors map[string][]storage.StationNeighbor, yclient ...*yandex.Client) *Itinerary {
|
func (g *Graph) CheckAndRescheduleRoute(itinerary *Itinerary, originID, destID string, opts SearchOptions, closedStations map[string]bool, neighbors map[string][]storage.StationNeighbor, yclient ...*yandex.Client) *Itinerary {
|
||||||
if g.checkRouteForChanges(itinerary) {
|
if g.checkRouteForChanges(itinerary) {
|
||||||
return g.rescheduleRoute(originID, destID, opts, closedStations, neighbors, yclient...)
|
result := g.rescheduleRoute(originID, destID, opts, closedStations, neighbors, yclient...)
|
||||||
|
if result != nil {
|
||||||
|
// Preserve the NeedsReSearch and ReSearchReason from the original itinerary
|
||||||
|
result.NeedsReSearch = itinerary.NeedsReSearch
|
||||||
|
result.ReSearchReason = itinerary.ReSearchReason
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
return itinerary
|
return itinerary
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -398,10 +398,10 @@ func TestRouteReSearchOnChange(t *testing.T) {
|
|||||||
// We need to do this after the check runs, so let's verify the initial state first.
|
// We need to do this after the check runs, so let's verify the initial state first.
|
||||||
|
|
||||||
// Verify that initial state has NeedsReSearch false (no changes simulated yet)
|
// Verify that initial state has NeedsReSearch false (no changes simulated yet)
|
||||||
if !itinerary.NeedsReSearch {
|
if itinerary.NeedsReSearch {
|
||||||
t.Log("PASS: Initial NeedsReSearch is false (no changes simulated)")
|
t.Errorf("expected initial NeedsReSearch to be false, got true")
|
||||||
} else {
|
} else {
|
||||||
t.Log("INFO: Initial NeedsReSearch is already true")
|
t.Log("PASS: Initial NeedsReSearch is false (no changes simulated)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now simulate cancellation by setting edge s1->s2 duration to > 86400 (1 day = cancellation)
|
// Now simulate cancellation by setting edge s1->s2 duration to > 86400 (1 day = cancellation)
|
||||||
@@ -413,6 +413,9 @@ func TestRouteReSearchOnChange(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset LastChecked to force re-check (bypass the 1-hour cache)
|
||||||
|
itinerary.LastChecked = time.Now().Unix() - 7200
|
||||||
|
|
||||||
// Re-check for changes after simulating cancellation
|
// Re-check for changes after simulating cancellation
|
||||||
checked2 := graph.CheckAndRescheduleRoute(itinerary, "s1", "s3", SearchOptions{MaxTransfers: 5}, nil, nil)
|
checked2 := graph.CheckAndRescheduleRoute(itinerary, "s1", "s3", SearchOptions{MaxTransfers: 5}, nil, nil)
|
||||||
t.Logf("After cancellation - NeedsReSearch: %v, ReSearchReason: %s", checked2.NeedsReSearch, checked2.ReSearchReason)
|
t.Logf("After cancellation - NeedsReSearch: %v, ReSearchReason: %s", checked2.NeedsReSearch, checked2.ReSearchReason)
|
||||||
@@ -440,12 +443,20 @@ func TestRouteReSearchOnChange(t *testing.T) {
|
|||||||
ReSearchReason: "",
|
ReSearchReason: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
// For major delay, the check uses: edge.Duration > leg.Cost*2 && leg.Cost > 0
|
// Reset the s1->s2 edge duration to normal value before testing major delay
|
||||||
// With Cost=500, threshold would be 1000. Setting duration to 2000 should trigger.
|
for _, edge := range graph.edges {
|
||||||
|
if edge.From.ID == "s1" && edge.To.ID == "s2" {
|
||||||
|
edge.Duration = 3600 // Reset to normal duration
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For major delay, the check uses: edge.Duration > leg.Duration*2 && leg.Duration > 0
|
||||||
|
// With leg.Duration=3600, threshold would be 7200. Setting duration to 8000 should trigger.
|
||||||
for _, edge := range graph.edges {
|
for _, edge := range graph.edges {
|
||||||
if edge.From.ID == "s2" && edge.To.ID == "s3" {
|
if edge.From.ID == "s2" && edge.To.ID == "s3" {
|
||||||
edge.Duration = 2000 // > 500*2 = 1000, should trigger major delay
|
edge.Duration = 8000 // > 3600*2 = 7200, should trigger major delay
|
||||||
t.Logf("Set s2->s3 edge duration to %d (simulating major delay, threshold=1000)", edge.Duration)
|
t.Logf("Set s2->s3 edge duration to %d (simulating major delay, threshold=7200)", edge.Duration)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
package routing
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"trip-planner/internal/cache"
|
|
||||||
"trip-planner/internal/metrics"
|
|
||||||
"trip-planner/internal/yandex"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SearchCacheService handles caching and on-demand Yandex /search calls.
|
|
||||||
type SearchCacheService struct {
|
|
||||||
cache *cache.CacheAside
|
|
||||||
yclient *yandex.Client
|
|
||||||
metrics *metrics.Metrics
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSearchCacheService creates a new search cache service.
|
|
||||||
func NewSearchCacheService(cacheStore cache.Cache, yclient *yandex.Client, m *metrics.Metrics) *SearchCacheService {
|
|
||||||
return &SearchCacheService{
|
|
||||||
cache: cache.NewCacheAside(cacheStore, m),
|
|
||||||
yclient: yclient,
|
|
||||||
metrics: m,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchWithCache performs a route search with caching support.
|
|
||||||
// It uses the cache-aside pattern: try cache first, then Yandex API, then write back to cache.
|
|
||||||
func (s *SearchCacheService) SearchWithCache(ctx context.Context, from, to, date string, opts SearchOptions) (*yandex.Response, error) {
|
|
||||||
// Generate cache key including far-term flag to distinguish near-term vs far-term searches
|
|
||||||
farTermFlag := "near"
|
|
||||||
if opts.FarTerm {
|
|
||||||
farTermFlag = "far"
|
|
||||||
}
|
|
||||||
searchKey := cache.GetSearchKeyWithFarTerm(from, to, date, farTermFlag)
|
|
||||||
|
|
||||||
// Try to get from cache first
|
|
||||||
fetchFunc := func() ([]byte, error) {
|
|
||||||
// If we reach here, it's a cache miss - perform on-demand Yandex /search call
|
|
||||||
return s.performYandexSearch(ctx, from, to, date, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get or set from cache with appropriate TTL based on far-term flag
|
|
||||||
isFarTerm := opts.FarTerm
|
|
||||||
data, err := s.cache.GetSearch(ctx, searchKey, fetchFunc, isFarTerm)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("search cache get/set: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the yandex.Response from cached data
|
|
||||||
var result yandex.Response
|
|
||||||
if err := json.Unmarshal(data, &result); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse yandex response from cache: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// performYandexSearch makes the actual Yandex /search API call.
|
|
||||||
func (s *SearchCacheService) performYandexSearch(ctx context.Context, from, to, date string, opts SearchOptions) ([]byte, error) {
|
|
||||||
// Build query parameters for Yandex /search endpoint
|
|
||||||
query := map[string]string{
|
|
||||||
"from": from,
|
|
||||||
"to": to,
|
|
||||||
"date": date,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the Yandex API request
|
|
||||||
resp, err := s.yclient.Do(ctx, "GET", "/v3.0/search/", query)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("yandex search failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert response to bytes for caching
|
|
||||||
return convertResponseToBytes(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertResponseToBytes converts Yandex API response to bytes for caching.
|
|
||||||
func convertResponseToBytes(resp *yandex.Response) ([]byte, error) {
|
|
||||||
if resp == nil {
|
|
||||||
return nil, fmt.Errorf("nil response")
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marshal response: %w", err)
|
|
||||||
}
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package routing
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"trip-planner/internal/cache"
|
|
||||||
"trip-planner/internal/metrics"
|
|
||||||
"trip-planner/internal/yandex"
|
|
||||||
)
|
|
||||||
|
|
||||||
// mockCacheStoreForSearch is a mock implementation of Cache for testing search cache
|
|
||||||
type mockCacheStoreForSearch struct {
|
|
||||||
data map[string][]byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockCacheStoreForSearch) Get(ctx context.Context, key *cache.CacheKey) ([]byte, error) {
|
|
||||||
keyStr := key.Kind + ":" + key.Code
|
|
||||||
if data, ok := m.data[keyStr]; ok {
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockCacheStoreForSearch) Set(ctx context.Context, key *cache.CacheKey, value []byte, ttl time.Duration) error {
|
|
||||||
keyStr := key.Kind + ":" + key.Code
|
|
||||||
m.data[keyStr] = value
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockCacheStoreForSearch) Exists(ctx context.Context, key *cache.CacheKey) (bool, error) {
|
|
||||||
keyStr := key.Kind + ":" + key.Code
|
|
||||||
_, ok := m.data[keyStr]
|
|
||||||
return ok, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockCacheStoreForSearch) Delete(ctx context.Context, key *cache.CacheKey) error {
|
|
||||||
keyStr := key.Kind + ":" + key.Code
|
|
||||||
delete(m.data, keyStr)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockCacheStoreForSearch) Increment(ctx context.Context, key *cache.CacheKey) (int64, error) {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockCacheStoreForSearch) Decrement(ctx context.Context, key *cache.CacheKey) (int64, error) {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewSearchCacheService(t *testing.T) {
|
|
||||||
mockStore := &mockCacheStoreForSearch{data: make(map[string][]byte)}
|
|
||||||
metrics := metrics.New()
|
|
||||||
yclient := yandex.NewClient("test-key")
|
|
||||||
|
|
||||||
svc := NewSearchCacheService(mockStore, yclient, metrics)
|
|
||||||
if svc == nil {
|
|
||||||
t.Error("expected SearchCacheService to be created")
|
|
||||||
}
|
|
||||||
if svc.cache == nil {
|
|
||||||
t.Error("expected cache to be initialized")
|
|
||||||
}
|
|
||||||
if svc.yclient == nil {
|
|
||||||
t.Error("expected yclient to be initialized")
|
|
||||||
}
|
|
||||||
if svc.metrics == nil {
|
|
||||||
t.Error("expected metrics to be initialized")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConvertResponseToBytes(t *testing.T) {
|
|
||||||
// Test with nil response
|
|
||||||
_, err := convertResponseToBytes(nil)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error for nil response")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with valid response
|
|
||||||
resp := &yandex.Response{
|
|
||||||
Segments: []yandex.Segment{},
|
|
||||||
}
|
|
||||||
data, err := convertResponseToBytes(resp)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no error, got %v", err)
|
|
||||||
}
|
|
||||||
if data == nil {
|
|
||||||
t.Error("expected non-nil data")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -197,10 +196,9 @@ func (c *Client) executeRequest(ctx context.Context, url string) (*Response, err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("request failed: %w", err)
|
return nil, fmt.Errorf("request failed: %w", err)
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
io.ReadAll(resp.Body) // Drain body to allow connection reuse
|
|
||||||
resp.Body.Close()
|
|
||||||
return nil, newAPIError(resp.StatusCode, resp.Status)
|
return nil, newAPIError(resp.StatusCode, resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,10 +277,10 @@ func isRetryableError(err error) bool {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Check for HTTP status codes that are retryable (5xx errors)
|
// Check for HTTP status codes that are retryable (5xx errors and 429)
|
||||||
apiErr, ok := err.(*APIError)
|
apiErr, ok := err.(*APIError)
|
||||||
if ok {
|
if ok {
|
||||||
return apiErr.Code >= 500 && apiErr.Code < 600
|
return (apiErr.Code >= 500 && apiErr.Code < 600) || apiErr.Code == 429
|
||||||
}
|
}
|
||||||
// Check for network errors
|
// Check for network errors
|
||||||
errStr := err.Error()
|
errStr := err.Error()
|
||||||
|
|||||||
Reference in New Issue
Block a user