Compare commits

...

11 Commits

Author SHA1 Message Date
JurajKubrican
d65c8b009c update
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
2026-01-26 10:42:28 +01:00
Jurajk
86b98e8c16 clean up stats
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
2025-12-20 21:58:52 +01:00
Jurajk
2bc1b780b8 security update 2025-12-02 13:43:14 +01:00
JurajKubrican
306db9bc1d timer 2025-10-22 11:09:22 +02:00
JurajKubrican
70219c9dd0 adding raw user agent to tracking 2025-09-08 13:11:08 +02:00
JurajKubrican
03a9dcff22 remove robots from db 2025-09-08 13:02:00 +02:00
JurajKubrican
caaa314065 tracking tweaks 2025-09-08 12:56:34 +02:00
JurajKubrican
0e7cf5d5cf update go 2025-09-08 12:50:48 +02:00
JurajKubrican
bc0841c3c0 improve tracking 2025-09-08 12:38:59 +02:00
JurajKubrican
0323212d9d real db migrations 2025-08-22 18:27:43 +02:00
JurajKubrican
74669f15f7 new build 2025-08-22 18:14:39 +02:00
24 changed files with 482 additions and 661 deletions

View File

@@ -7,7 +7,3 @@ BUILD_NUMBER=prod-v1.0
# Grafana settings
GRAFANA_PASSWORD=admin
# Optional: Custom Prometheus retention
# PROMETHEUS_RETENTION_TIME=30d
# PROMETHEUS_RETENTION_SIZE=10GB

View File

@@ -17,6 +17,19 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '1.25'
- name: Install dependencies
run: |
go mod download
go mod tidy
- name: Build Go application
run: |
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v -o ./server ./src
- name: Build Docker image
run: |
docker build -t "$IMAGE_NAME:latest" -t "$IMAGE_NAME:${{ github.run_number }}" .
@@ -32,7 +45,7 @@ jobs:
run: |
docker run -d --name temp-$IMAGE_NAME -p $TEST_PORT:54321 $IMAGE_NAME:latest
sleep 10 # Wait for the container to start
if curl -f http://localhost:$TEST_PORT/health; then
if curl -f --max-time 10 http://localhost:$TEST_PORT/health; then
echo "Health check passed"
docker stop temp-$IMAGE_NAME
docker rm temp-$IMAGE_NAME

View File

@@ -1,58 +0,0 @@
version: '3.8'
services:
knet:
build: .
ports:
- "54321:54321"
environment:
- API_TOKEN=${API_TOKEN:-your-default-token}
- API_USERNAME=${API_USERNAME:-api}
- BUILD_NUMBER=${BUILD_NUMBER:-dev}
networks:
- monitoring
prometheus:
image: prom/prometheus:latest
container_name: knet-prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=30d'
- '--storage.tsdb.retention.size=10GB'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--web.enable-lifecycle'
- '--web.enable-admin-api'
networks:
- monitoring
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: knet-grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin}
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
networks:
- monitoring
restart: unless-stopped
depends_on:
- prometheus
networks:
monitoring:
driver: bridge
volumes:
prometheus_data:
grafana_data:

View File

@@ -1,29 +1,15 @@
# Step 1: Build the Go binary in a separate stage
FROM golang:1.24 AS builder
# Set the working directory
WORKDIR /app
# Copy the Go module files
COPY go.mod go.sum ./
RUN go mod download
# Copy the rest of the application code
COPY ./src ./src
# Build the Go binary with static linking for Linux
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v -o /server ./src
COPY ./views ./views
COPY ./build_number ./build_number
FROM scratch
# Set the current working directory inside the container
WORKDIR /app
COPY . .
COPY --from=builder server ./
COPY ./server .
COPY ./views ./views
COPY ./data ./data
COPY ./js ./js
COPY ./css ./css
COPY ./build_number ./build_number
# COPY --from=builder server ./
# Command to run the executable
CMD ["./server"]

View File

@@ -1,173 +0,0 @@
# Grafana Integration Guide
This guide shows how to visualize your application metrics with Grafana using **real Prometheus**.
## Why Prometheus?
We've replaced custom tracking with actual Prometheus because:
-**Industry standard** - Battle-tested metrics collection
-**Rich ecosystem** - Thousands of existing dashboards
-**Better performance** - Optimized time-series storage
-**Less code** - No custom database or API endpoints
-**Standard metrics** - HTTP requests, response times, etc.
## Available Metrics
### Automatic Metrics (from echo-contrib/prometheus)
- `http_requests_total` - Total HTTP requests by method/status/endpoint
- `http_request_duration_seconds` - Request duration histogram
- `http_request_size_bytes` - Request size histogram
- `http_response_size_bytes` - Response size histogram
### Custom Application Metrics
- `knet_page_views_total{route="/"}` - Page views by route
- `knet_page_views_total{route="/draw"}` - Draw page views
- `knet_box_clicks_total{position="1"}` - Box clicks by position
- `knet_websocket_connections` - Active WebSocket connections
## Authentication
The `/metrics` endpoint uses **Basic Authentication** in production:
- **Username:** `api` (or set via `API_USERNAME` environment variable)
- **Password:** Your API token (set via `API_TOKEN` environment variable)
**Example:**
```bash
curl -u api:your-api-token http://your-server:54321/metrics
```
## Grafana Setup
### Prometheus Data Source (Recommended)
1. **Add Prometheus Data Source** in Grafana
2. **Set URL** to: `http://your-server:54321`
3. **Set Basic Auth**: Username: `api`, Password: `your-api-token`
4. **Set Scrape Interval**: 15s (Prometheus scrapes our `/metrics` endpoint)
That's it! Grafana will automatically discover all available metrics.
### Sample Dashboard Queries
**Traffic Overview:**
```promql
# Total requests per minute
rate(http_requests_total[1m]) * 60
# Page views
knet_page_views_total
# Request duration 95th percentile
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
# Error rate
rate(http_requests_total{status=~"4.."}[5m]) / rate(http_requests_total[5m]) * 100
```
**Application Metrics:**
```promql
# Box interactions
rate(knet_box_clicks_total[5m])
# Active WebSocket connections
knet_websocket_connections
# Most popular pages
topk(10, rate(knet_page_views_total[1h]))
```
## Dashboard Examples
### 1. HTTP Overview Panel
- **Metric:** `rate(http_requests_total[1m]) * 60`
- **Type:** Graph
- **Title:** "Requests per Minute"
### 2. Response Time Panel
- **Metric:** `histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))`
- **Type:** Single Stat
- **Title:** "95th Percentile Response Time"
### 3. Page Views Panel
- **Metric:** `knet_page_views_total`
- **Type:** Graph
- **Title:** "Page Views by Route"
### 4. Box Clicks Heatmap
- **Metric:** `rate(knet_box_clicks_total[5m])`
- **Type:** Graph
- **Title:** "Box Clicks per Second"
## Environment Variables
Set these for production:
```bash
export API_TOKEN="your-secure-api-token"
export API_USERNAME="api" # Optional, defaults to "api"
export BUILD_NUMBER="prod-v1.0" # Enables production mode
```
## Benefits of This Approach
1. **Standard Prometheus** - Works with all Grafana features
2. **Rich Metrics** - HTTP performance + custom business metrics
3. **No Database** - Prometheus handles storage and retention
4. **Better Performance** - Optimized time-series queries
5. **Alerting Ready** - Use Prometheus AlertManager
6. **Ecosystem** - Tons of existing dashboards and tools
## Persistent Storage Setup
### Option 1: Docker Compose (Recommended)
For persistent metrics storage, run a Prometheus server alongside your app:
```bash
# 1. Copy your API token to .env
cp .env.example .env
# Edit .env and set your API_TOKEN
# 2. Start the full monitoring stack
docker-compose up -d
# 3. Access services
# - Your app: http://localhost:54321
# - Prometheus: http://localhost:9090
# - Grafana: http://localhost:3000 (admin/admin)
```
**What this gives you:**
-**Persistent metrics** - Data survives restarts
-**30-day retention** - Configurable in docker-compose.yml
-**Grafana pre-configured** - Points to Prometheus automatically
-**Production ready** - Proper volumes and networking
### Option 2: Standalone Prometheus
If you prefer to run Prometheus separately:
```bash
# 1. Update prometheus.yml with your API token
# 2. Start Prometheus
prometheus --config.file=prometheus.yml --storage.tsdb.path=./prometheus_data
# 3. Configure Grafana to point to http://localhost:9090
```
### Grafana Data Source Configuration
With persistent Prometheus:
1. **Add Prometheus Data Source** in Grafana
2. **Set URL** to: `http://prometheus:9090` (Docker) or `http://localhost:9090` (standalone)
3. **No authentication needed** - Prometheus handles the API token
4. **Query retention**: Up to 30 days of historical data
## Quick Start
1. **Start your server:** `./tmp/main`
2. **Test metrics:** `curl -u api:your-token http://localhost:54321/metrics`
3. **Add Prometheus data source** in Grafana: `http://localhost:54321`
4. **Create dashboard** with queries like `http_requests_total`
5. **Enjoy real-time metrics!** 📊
Your analytics are now powered by industry-standard Prometheus! <20>

View File

@@ -1,164 +0,0 @@
# Tracking API Documentation
The tracking service provides analytics about endpoint usage with time-based filtering.
## Authentication
In production, all tracking endpoints require Bearer token authentication:
```
Authorization: Bearer <your-api-token>
```
## Endpoints
### GET /api/tracking/stats
Get endpoint statistics with optional time period filtering.
**Query Parameters:**
- `period` (optional): `day`, `week`, `month`, `year`, `all` (default: `all`)
**Examples:**
```bash
# Get all-time stats
curl -H "Authorization: Bearer $API_TOKEN" /api/tracking/stats
# Get last week's stats
curl -H "Authorization: Bearer $API_TOKEN" /api/tracking/stats?period=week
# Get last 30 days
curl -H "Authorization: Bearer $API_TOKEN" /api/tracking/stats?period=month
```
**Response:**
```json
{
"stats": [
{
"endpoint": "/",
"method": "GET",
"hit_count": 245,
"unique_users": 89,
"last_access": "2025-08-13T10:30:00Z",
"first_access": "2025-08-06T14:22:00Z"
}
],
"period": "week",
"total_endpoints": 8
}
```
### GET /api/tracking/summary
Get overall tracking summary for a time period.
**Query Parameters:**
- `period` (optional): `day`, `week`, `month`, `year`, `all` (default: `all`)
**Examples:**
```bash
# Get today's summary
curl -H "Authorization: Bearer $API_TOKEN" /api/tracking/summary?period=day
# Get yearly summary
curl -H "Authorization: Bearer $API_TOKEN" /api/tracking/summary?period=year
```
**Response:**
```json
{
"period": "day",
"total_hits": 1247,
"total_endpoints": 12,
"total_unique_users": 234,
"oldest_visit": "2025-08-13T00:15:00Z",
"newest_visit": "2025-08-13T23:45:00Z",
"tracking_active": true
}
```
### GET /api/tracking/trends
Get visit trends over time with configurable granularity.
**Query Parameters:**
- `granularity` (optional): `hour`, `day`, `week`, `month` (default: `day`)
- `period` (optional): `24h`, `7d`, `30d`, `1y` (default: `7d`)
**Examples:**
```bash
# Hourly trends for last 24 hours
curl -H "Authorization: Bearer $API_TOKEN" /api/tracking/trends?granularity=hour&period=24h
# Daily trends for last 30 days
curl -H "Authorization: Bearer $API_TOKEN" /api/tracking/trends?granularity=day&period=30d
# Weekly trends for last year
curl -H "Authorization: Bearer $API_TOKEN" /api/tracking/trends?granularity=week&period=1y
```
**Response:**
```json
{
"trends": [
{
"time_bucket": "2025-08-13",
"hits": 567,
"unique_users": 123,
"unique_endpoints": 8
},
{
"time_bucket": "2025-08-12",
"hits": 445,
"unique_users": 98,
"unique_endpoints": 7
}
],
"granularity": "day",
"period": "7d"
}
```
### GET /api/tracking/visits
Get recent individual visits with optional filtering.
**Query Parameters:**
- `limit` (optional): Number of visits to return (default: `50`)
**Example:**
```bash
curl -H "Authorization: Bearer $API_TOKEN" /api/tracking/visits?limit=20
```
**Response:**
```json
{
"visits": [
{
"user_hash": "a1b2c3d4e5f6...",
"endpoint": "/boxes",
"method": "GET",
"ip_address": "192.168.1.100",
"user_agent": "Mozilla/5.0...",
"referer": "https://google.com",
"timestamp": "2025-08-13T15:30:00Z"
}
],
"count": 20
}
```
## Benefits of This Approach
1. **No Duplication**: Single source of truth in `user_visits` table
2. **Flexible Querying**: Any time period can be queried on-demand
3. **Rich Analytics**: Detailed trends and user behavior insights
4. **Efficient Storage**: Only raw visits stored, stats calculated in real-time
5. **Historical Analysis**: Can analyze any historical period without pre-aggregation
## Performance Considerations
- Indexes are created on `timestamp`, `endpoint`, and `user_hash` for fast queries
- For very high traffic, consider adding materialized views for common time periods
- The SQLite database handles thousands of requests efficiently with proper indexing

35
go.mod
View File

@@ -1,39 +1,32 @@
module knet.sk
go 1.24.0
go 1.25
require (
github.com/labstack/echo-contrib v0.17.4
github.com/labstack/echo/v4 v4.13.4
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/labstack/echo/v4 v4.15.0
github.com/labstack/gommon v0.4.2
github.com/prometheus/client_golang v1.23.0
golang.org/x/net v0.42.0
modernc.org/sqlite v1.38.2
github.com/mileusna/useragent v1.3.5
golang.org/x/net v0.49.0
modernc.org/sqlite v1.44.3
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mileusna/useragent v1.3.5 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
modernc.org/libc v1.66.3 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.14.0 // indirect
modernc.org/libc v1.67.7 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)

141
go.sum
View File

@@ -1,92 +1,105 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk=
github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
github.com/labstack/echo/v4 v4.14.0 h1:+tiMrDLxwv6u0oKtD03mv+V1vXXB3wCqPHJqPuIe+7M=
github.com/labstack/echo/v4 v4.14.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
github.com/labstack/echo/v4 v4.15.0 h1:hoRTKWcnR5STXZFe9BmYun9AMTNeSbjHi2vtDuADJ24=
github.com/labstack/echo/v4 v4.15.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
modernc.org/libc v1.67.1 h1:bFaqOaa5/zbWYJo8aW0tXPX21hXsngG2M7mckCnFSVk=
modernc.org/libc v1.67.1/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
modernc.org/libc v1.67.7 h1:H+gYQw2PyidyxwxQsGTwQw6+6H+xUk+plvOKW7+d3TI=
modernc.org/libc v1.67.7/go.mod h1:UjCSJFl2sYbJbReVQeVpq/MgzlbmDM4cRHIYFelnaDk=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -95,8 +108,12 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/sqlite v1.41.0 h1:bJXddp4ZpsqMsNN1vS0jWo4IJTZzb8nWpcgvyCFG9Ck=
modernc.org/sqlite v1.41.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -0,0 +1,4 @@
DROP INDEX IF EXISTS idx_user_visits_path;
DROP INDEX IF EXISTS idx_user_visits_fingerprint;
DROP INDEX IF EXISTS idx_user_visits_timestamp;
DROP TABLE IF EXISTS user_visits;

View File

@@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS user_visits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip TEXT NOT NULL,
path TEXT NOT NULL,
method TEXT NOT NULL,
fingerprint TEXT,
user_agent TEXT,
referer TEXT,
accept_language TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_user_visits_timestamp ON user_visits(timestamp);
CREATE INDEX IF NOT EXISTS idx_user_visits_fingerprint ON user_visits(fingerprint);
CREATE INDEX IF NOT EXISTS idx_user_visits_path ON user_visits(path);

View File

@@ -0,0 +1,5 @@
-- This migration is irreversible as we're deleting historical data
-- If needed, you would need to restore from a backup
-- No-op down migration since we can't restore deleted tracking events
SELECT 1;

View File

@@ -0,0 +1,29 @@
-- Remove tracking events that match current ignored patterns
-- Remove visits to ignored paths
DELETE FROM user_visits WHERE
path = '/tracking' OR
path = '/metrics' OR
path LIKE '/css/%' OR
path LIKE '/js/%' OR
path = '/boxes/ws' OR
path LIKE '%favicon%';
-- Remove visits from ignored user agents
DELETE FROM user_visits WHERE
user_agent LIKE '%Prometheus%' OR
user_agent LIKE '%UptimeRobot%' OR
user_agent LIKE 'NetworkingExtension%';
-- Optional: Clean up any other common bot/monitoring patterns that might exist
DELETE FROM user_visits WHERE
user_agent LIKE '%bot%' OR
user_agent LIKE '%Bot%' OR
user_agent LIKE '%crawler%' OR
user_agent LIKE '%Crawler%' OR
user_agent LIKE '%spider%' OR
user_agent LIKE '%Spider%' OR
user_agent LIKE '%monitor%' OR
user_agent LIKE '%Monitor%' OR
user_agent LIKE '%health%' OR
user_agent LIKE '%Health%';

View File

@@ -0,0 +1,5 @@
-- This migration is irreversible as we're deleting historical data
-- If needed, you would need to restore from a backup
-- No-op down migration since we can't restore deleted tracking events
SELECT 1;

View File

@@ -0,0 +1,10 @@
-- Remove tracking events that match current ignored patterns
-- Remove visits to ignored paths
DELETE FROM user_visits WHERE
'path' = '/tracking' OR
'path' = '/metrics' OR
'path' LIKE '/css/%' OR
'path' LIKE '/js/%' OR
'path' = '/boxes/ws' OR
'path' LIKE '%favicon%';

View File

@@ -19,7 +19,7 @@
"dev": "tsc --project tsconfig.json --watch"
},
"devDependencies": {
"typescript": "^5.7.3"
"typescript": "^5.9.2"
},
"dependencies": {
"mitt": "^3.0.1"

10
pnpm-lock.yaml generated
View File

@@ -13,16 +13,16 @@ importers:
version: 3.0.1
devDependencies:
typescript:
specifier: ^5.7.3
version: 5.7.3
specifier: ^5.9.2
version: 5.9.2
packages:
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
typescript@5.7.3:
resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==}
typescript@5.9.2:
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
engines: {node: '>=14.17'}
hasBin: true
@@ -30,4 +30,4 @@ snapshots:
mitt@3.0.1: {}
typescript@5.7.3: {}
typescript@5.9.2: {}

View File

@@ -1,68 +0,0 @@
# Prometheus configuration for knet application
global:
scrape_interval: 15s # How often to scrape targets
evaluation_interval: 15s # How often to evaluate rules
# Attach these labels to any time series or alerts when communicating with
# external systems (federation, remote storage, Alertmanager).
external_labels:
monitor: 'knet-monitor'
# Alertmanager configuration (optional)
alerting:
alertmanagers:
- static_configs:
- targets:
# - alertmanager:9093
# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"
# A scrape configuration containing exactly one endpoint to scrape:
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: 'knet-app'
# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.
# How often to scrape this target
scrape_interval: 15s
# Timeout for scraping
scrape_timeout: 10s
# Use HTTPS to connect to the target
scheme: https
# Basic authentication for protected endpoints
basic_auth:
username: 'api'
password: 'your-default-token' # This should match your API_TOKEN env var
static_configs:
- targets: ['knet.sk'] # Your knet application
labels:
instance: 'knet-prod'
environment: 'production'
# Only scrape metrics endpoint
metrics_path: /metrics
# Optional: Add custom headers
# headers:
# X-Custom-Header: value
# Optional: Monitor Prometheus itself
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Storage configuration
# Prometheus stores data in ./data by default
# You can customize with command line flags:
# --storage.tsdb.path=/prometheus
# --storage.tsdb.retention.time=15d
# --storage.tsdb.retention.size=10GB

View File

@@ -4,16 +4,12 @@ import (
"html/template"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/labstack/echo-contrib/prometheus"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/gommon/log"
promclient "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"knet.sk/src/boxes"
"knet.sk/src/draw"
"knet.sk/src/tracking"
@@ -50,46 +46,16 @@ func newPage(boxes []boxes.Box) Page {
var (
e = echo.New()
// Custom Prometheus metrics
boxClicks = promclient.NewCounterVec(
promclient.CounterOpts{
Name: "knet_box_clicks_total",
Help: "Total number of box clicks by position",
},
[]string{"position"},
)
activeConnections = promclient.NewGauge(
promclient.GaugeOpts{
Name: "knet_websocket_connections",
Help: "Current number of active WebSocket connections",
},
)
pageViews = promclient.NewCounterVec(
promclient.CounterOpts{
Name: "knet_page_views_total",
Help: "Total page views by route",
},
[]string{"route"},
)
)
func init() {
// Register custom metrics
promclient.MustRegister(boxClicks)
promclient.MustRegister(activeConnections)
promclient.MustRegister(pageViews)
}
func main() {
e.Renderer = NewTemplates()
// Setup Prometheus metrics
p := prometheus.NewPrometheus("knet", nil)
p.Use(e)
e.Logger.SetLevel(log.DEBUG)
e.Use(middleware.Logger())
if util.IsProd() {
@@ -101,27 +67,13 @@ func main() {
}))
}
// MY supper tracking
e.Use(tracking.EchoWithConfig(tracking.TrackingConfig{
IgnorePaths: []string{"/tracking", "/metrics", "/css/*", "/js/*", "/boxes/ws"},
IgnoreUserAgents: []string{"*Prometheus*", "*UptimeRobot*"},
IgnorePaths: []string{"/tracking", "/metrics", "/css/*", "/js/*", "/boxes/ws", "*favicon*"},
IgnoreUserAgents: []string{},
}))
trackingGroup := e.Group("/tracking")
trackingGroup.GET("", tracking.BasicTrackingHandler)
// Prometheus metrics endpoint (standard /metrics)
metricsGroup := e.Group("/metrics")
if util.IsProd() {
metricsGroup.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
expectedToken := os.Getenv("API_TOKEN")
if expectedToken == "" {
return false, nil
}
return username == "api" && password == expectedToken, nil
}))
}
metricsGroup.GET("", echo.WrapHandler(promhttp.Handler()))
e.GET("/health", func(c echo.Context) error {
return c.Render(200, "health", Page{
BuildNumber: util.GetBuildNumber(),
@@ -145,15 +97,21 @@ func main() {
e.GET("/", func(c echo.Context) error {
// Track page view
pageViews.WithLabelValues("/").Inc()
return c.Render(200, "index", newPage(boxes.GetBoxes()))
})
e.GET("/timer", func(c echo.Context) error {
// Track page view
return c.Render(200, "timer", Page{
BuildNumber: util.GetBuildNumber(),
Integrity: util.CalculateAssetIntegrities(),
})
})
e.GET("/boxes/ws", boxes.HandleBoxesWs)
e.GET("/draw", func(c echo.Context) error {
// Track page view
pageViews.WithLabelValues("/draw").Inc()
return draw.Page(c)
})
e.GET("/draw/ws", draw.InitWs)

View File

@@ -1,20 +0,0 @@
package main
import (
"github.com/prometheus/client_golang/prometheus"
)
// GetActiveConnections returns the current number of active WebSocket connections
// This function will be called by Prometheus to get the current gauge value
func GetActiveConnections() prometheus.GaugeFunc {
return prometheus.NewGaugeFunc(
prometheus.GaugeOpts{
Name: "knet_websocket_connections_active",
Help: "Current number of active WebSocket connections",
},
func() float64 {
// This will be implemented to call boxes.GetConnectionCount()
return 0 // Placeholder for now
},
)
}

52
src/migrations/manager.go Normal file
View File

@@ -0,0 +1,52 @@
package migrations
import (
"database/sql"
"fmt"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
type MigrationManager struct {
databases map[string]*sql.DB
}
func NewMigrationManager() *MigrationManager {
return &MigrationManager{
databases: make(map[string]*sql.DB),
}
}
// RegisterDatabase registers a database connection for migrations
func (mm *MigrationManager) RegisterDatabase(name string, db *sql.DB) {
mm.databases[name] = db
}
// RunMigrations runs migrations for a specific database
func (mm *MigrationManager) RunMigrations(dbName string) error {
db, exists := mm.databases[dbName]
if !exists {
return fmt.Errorf("database %s not registered", dbName)
}
driver, err := sqlite.WithInstance(db, &sqlite.Config{})
if err != nil {
return fmt.Errorf("failed to create database driver: %w", err)
}
migrationPath := fmt.Sprintf("file://migrations/%s", dbName)
m, err := migrate.NewWithDatabaseInstance(migrationPath, "sqlite", driver)
if err != nil {
return fmt.Errorf("failed to create migration instance: %w", err)
}
// Run migrations
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
return fmt.Errorf("failed to run migrations: %w", err)
}
fmt.Printf("Migrations completed successfully for database: %s\n", dbName)
return nil
}

View File

@@ -21,6 +21,7 @@ type VisitDisplay struct {
IPAddress string
Path string
UserAgent string
RawUserAgent string
Referrer string
}
@@ -77,7 +78,7 @@ func BasicTrackingHandler(c echo.Context) error {
UniqueVisitors: ts.GetUniqueVisitors(),
TodayVisits: ts.GetTodayVisits(),
ActiveSessions: ts.GetActiveSessions(),
RecentVisits: ts.GetRecentVisits(20),
RecentVisits: ts.GetRecentVisits(100),
TopPages: ts.GetTopPages(5),
}

View File

@@ -2,10 +2,12 @@ package tracking
import (
"database/sql"
"log"
"sync"
"time"
"github.com/labstack/echo/v4"
"knet.sk/src/migrations"
_ "modernc.org/sqlite"
)
@@ -43,10 +45,19 @@ func StartTrackingService() *TrackingService {
db.SetMaxIdleConns(1)
trackingService = &TrackingService{db: db}
err = trackingService.initDB()
if err != nil {
// Run migrations instead of manual table creation
migrationManager := migrations.NewMigrationManager()
migrationManager.RegisterDatabase("tracking", db)
if err := migrationManager.RunMigrations("tracking"); err != nil {
println("Warning: Migration failed:", err.Error())
// Fallback to manual table creation for backward compatibility
if err := trackingService.initDB(); err != nil {
panic(err)
}
} else {
log.Println("Tracking database migrations applied successfully.")
}
}
return trackingService
@@ -190,6 +201,8 @@ func (ts *TrackingService) GetRecentVisits(limit int) []VisitDisplay {
visit.Timestamp = rawTimestamp
}
// Store raw user agent before parsing
visit.RawUserAgent = visit.UserAgent
visit.UserAgent = parseUserAgent(visit.UserAgent)
visits = append(visits, visit)
}

207
views/timer.html Normal file
View File

@@ -0,0 +1,207 @@
{{block "timer" .}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>November Countdown</title>
<link rel="stylesheet" href="/css/main.css?v={{.BuildNumber}}" integrity="{{index .Integrity.CSS "main.css" }}" crossorigin="anonymous" />
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" type="image/x-icon" href="/images/favicon.ico" />
<style>
.countdown-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
text-align: center;
}
.countdown-title {
font-size: 2rem;
margin-bottom: 2rem;
color: var(--color);
}
.countdown-timer {
font-family: var(--font-family);
font-size: 4rem;
font-weight: bold;
color: var(--secondary);
margin-bottom: 1rem;
letter-spacing: 0.1em;
text-shadow: 0 0 10px rgba(197, 134, 192, 0.3);
}
.countdown-labels {
display: flex;
gap: 3rem;
font-size: 0.9rem;
color: var(--color);
opacity: 0.8;
}
.countdown-unit {
display: flex;
flex-direction: column;
align-items: center;
}
.countdown-number {
font-size: 4rem;
font-weight: bold;
color: var(--secondary);
line-height: 1;
margin-bottom: 0.5rem;
}
.countdown-label {
font-size: 0.9rem;
color: var(--color);
opacity: 0.8;
text-transform: uppercase;
letter-spacing: 0.1em;
}
@media (max-width: 768px) {
.countdown-timer {
font-size: 2.5rem;
}
.countdown-title {
font-size: 1.5rem;
}
.countdown-number {
font-size: 2.5rem;
}
.countdown-labels {
gap: 1.5rem;
}
}
@media (max-width: 480px) {
.countdown-timer {
font-size: 2rem;
}
.countdown-title {
font-size: 1.2rem;
}
.countdown-number {
font-size: 2rem;
}
.countdown-labels {
gap: 1rem;
}
}
</style>
</head>
<body>
<header>
<nav class="logo">
<a href="/">
<svg width="64" height="64" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Background circle for better visibility -->
<circle cx="16" cy="16" r="15" fill="#1e1e1e" stroke="var(--color)" stroke-width="2"/>
<!-- Vertical line of K -->
<line x1="10" y1="8" x2="10" y2="24" stroke="var(--color)" stroke-width="2" stroke-linecap="round"/>
<!-- Upper diagonal of K -->
<line x1="10" y1="16" x2="22" y2="8" stroke="var(--color)" stroke-width="2" stroke-linecap="round"/>
<!-- Lower diagonal of K -->
<line x1="10" y1="16" x2="22" y2="24" stroke="var(--color)" stroke-width="2" stroke-linecap="round"/>
</svg>
</a>
</nav>
</header>
<main>
<div class="countdown-container">
<h1 class="countdown-title">Countdown to December 1st</h1>
<div class="countdown-display">
<div class="countdown-labels">
<div class="countdown-unit">
<div class="countdown-number" id="days">00</div>
<div class="countdown-label">Days</div>
</div>
<div class="countdown-unit">
<div class="countdown-number" id="hours">00</div>
<div class="countdown-label">Hours</div>
</div>
<div class="countdown-unit">
<div class="countdown-number" id="minutes">00</div>
<div class="countdown-label">Minutes</div>
</div>
<div class="countdown-unit">
<div class="countdown-number" id="seconds">00</div>
<div class="countdown-label">Seconds</div>
</div>
</div>
</div>
</div>
</main>
<footer>
<nav>
<ul>
<li>
<a href="https://www.goodreads.com/jurajk">Goodreads</a>
</li>
<li>
<a href="https://open.spotify.com/playlist/1X6PkYyptMu0kOFhVGTTkS?si=f55894a8c1e74f42">Current vibe Spotify Playlist</a>
</li>
<li>
<a href="https://github.com/JurajKubrican">Github</a>
</li>
<li>
<a href="https://www.linkedin.com/in/juraj-kubri%C4%8Dan-614b3274/">LinkedIn</a >
</li>
</ul>
</nav>
</footer>
<script>
function updateCountdown() {
const now = new Date().getTime();
const currentYear = new Date().getFullYear();
// Target is December 1st of current year at 00:00:00
// If we're already past December 1st, target next year's December 1st
let targetDate = new Date(currentYear, 11, 1, 0, 0, 0, 0); // Month 11 = December
if (now > targetDate.getTime()) {
targetDate = new Date(currentYear + 1, 11, 1, 0, 0, 0, 0);
}
const distance = targetDate.getTime() - now;
if (distance < 0) {
document.getElementById('days').textContent = '00';
document.getElementById('hours').textContent = '00';
document.getElementById('minutes').textContent = '00';
document.getElementById('seconds').textContent = '00';
return;
}
const days = Math.floor(distance / (1000 * 60 * 60 * 24));
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
document.getElementById('days').textContent = days.toString().padStart(2, '0');
document.getElementById('hours').textContent = hours.toString().padStart(2, '0');
document.getElementById('minutes').textContent = minutes.toString().padStart(2, '0');
document.getElementById('seconds').textContent = seconds.toString().padStart(2, '0');
}
// Update countdown immediately and then every second
updateCountdown();
setInterval(updateCountdown, 1000);
</script>
</body>
</html>
{{end}}

View File

@@ -114,7 +114,7 @@
<td>{{.Timestamp}}</td>
<td>{{.IPAddress}}</td>
<td>{{.Path}}</td>
<td>{{.UserAgent}}</td>
<td title="{{.RawUserAgent}}">{{.UserAgent}}</td>
<td>{{.Referrer}}</td>
</tr>
{{else}}