Compare commits

8 Commits

Author SHA1 Message Date
nineap d9b2e24b8e Переименовать GITEA_REGISTRY_TOKEN → GT_REGISTRY_TOKEN (правила именования Gitea не пускают префикс GITEA_)
Build & Push Image / Build and push to Gitea Registry (push) Successful in 19m50s
2026-07-10 21:28:25 +07:00
nineap df699a1171 Переезд с ghcr.io на встроенный Container Registry Gitea
Build & Push Image / Build and push to Gitea Registry (push) Failing after 15s
Свой netrunner-landing/netrunner-landing-proxy теперь на
gitea.netrunner-vpn.com. Сторонний образ PocketBase (ghcr.io/muchobien/
pocketbase) не трогаю — это чужой образ, живёт там же, где и жил.
2026-07-10 20:44:14 +07:00
nineap 7076bbcc78 nginx+TLS перед лендингом на прод VPS вместо голых портов наружу
Порты 3000/3001 закрыты для интернета (ufw allow только с IP VPS данных —
Prometheus). Публичный вход только netrunner-vpn.com через nginx: "/" на
сам лендинг, "/cms-api/" на PocketBase (со сменой префикса на "/api"),
"/api/" на proxy-ws (comments/speedtest, уже с этим префиксом внутри).
2026-07-10 19:07:10 +07:00
nineap bc500c8716 CI сам собирает .env на dev VPS (BOT_USERNAME) из repo Variables
Тот же принцип, что и в netrunner-backend/netrunner-data — deploy-dev
пишет .env из vars.DEV_BOT_USERNAME перед docker compose up, руками на
сервере создавать больше не нужно. Прод по-прежнему без .env — все
значения там хардкожены в docker-compose.yml.
2026-07-10 18:18:57 +07:00
nineap 383077e1e6 Метрики /metrics для Next.js и ws-proxy (центральный Prometheus)
proxy-ws (Rust/axum) — тот же паттерн, что api.rs в netrunner-backend:
PrometheusBuilder + middleware, считающий http_requests_total/
http_request_duration_seconds по методу/пути/статусу.

Next.js — app/api/metrics/route.ts на prom-client (явный
runtime="nodejs", т.к. Edge Runtime не тянет perf_hooks/process.*),
отдаёт default-метрики процесса (CPU/память/event loop lag).

Оба порта (3000/3001) перестают быть 127.0.0.1-only — их должен
скрейпить центральный Prometheus на VPS с данными, обязательно
зафайрволить там на его IP (см. комментарии в docker-compose.yml).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 23:21:43 +07:00
nineap d99026d13b CI: VPS_IP/DEV_VPS_IP переносит из secrets в vars
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 16:37:06 +07:00
nineap 0fa87e2d9a Фолбэк на кэш при недоступности PocketBase (Pricing/FAQ)
Pricing: при полном отказе всех запросов пробует localStorage-кэш
последнего успешного ответа вместо пустого состояния, пишет туда же при
успехе. Faq: отличает "в кэше правда пусто" от "не смогли загрузить" —
раньше оба случая показывали один и тот же текст "0 protocols found",
что вводило в заблуждение при обычном сетевом сбое.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 16:37:06 +07:00
nineap b8bf9f7337 Добавляет .env.example + чинит .gitignore (widlcard .env* его же и ронял)
.gitignore имел общий паттерн ".env*" (стандартный для Next.js) без
исключения для .env.example — сам шаблон никогда не мог быть закоммичен.
Добавлено исключение !.env.example.

docker-compose.dev-remote.yml требует ${BOT_USERNAME:?...} — жёсткая
зависимость от .env на dev VPS, нигде не задокументированная до этого.
Прод (docker-compose.yml) секретов не требует — там всё захардкожено
(публичные URL/домены, ни одного пароля/токена).
2026-07-09 15:35:10 +07:00
30 changed files with 850 additions and 79 deletions
+11
View File
@@ -0,0 +1,11 @@
# УСТАРЕЛО КАК ИНСТРУКЦИЯ: .env на dev VPS теперь собирает сам deploy.yml
# (job deploy-dev, vars.DEV_BOT_USERNAME) — руками копировать не нужно.
#
# Только для dev-remote (docker-compose.dev-remote.yml). Прод
# (docker-compose.yml) секретов не требует — все URL/домены захардкожены
# прямо в нём (там нет ни одного пароля/токена, только публичные адреса).
#
# ОБЯЗАТЕЛЬНО: отдельный dev-бот, заведённый через @BotFather специально под
# домен dev.netrunner-vpn.com — НЕ прод-бот (у него уже прописан прод-домен
# через /setdomain, молчаливый фолбэк на него ловится как "Bot domain invalid").
BOT_USERNAME=CHANGE_ME_DEV_BOT_USERNAME
+16 -13
View File
@@ -1,6 +1,9 @@
# Копия .github/workflows/build.yml для Gitea Actions. Логин на ghcr.io — через # Копия .github/workflows/build.yml для Gitea Actions. Реестр — встроенный
# secrets.GHCR_TOKEN (настоящий GitHub PAT с write:packages), не # Container Registry самой Gitea (gitea.netrunner-vpn.com), не ghcr.io —
# secrets.GITHUB_TOKEN — автотокен Gitea валиден только для самого Gitea. # secrets.GT_REGISTRY_TOKEN (токен Gitea с правом write:package), не
# GHCR_TOKEN/GITHUB_TOKEN. Тот же токен, что и в netrunner-backend/
# netrunner-proxy — пакеты в Gitea принадлежат аккаунту, не репозиторию,
# но значение секрета нужно продублировать в Settings каждого репозитория.
name: Build & Push Image name: Build & Push Image
on: on:
@@ -10,7 +13,7 @@ on:
jobs: jobs:
build: build:
name: Build and push to GHCR name: Build and push to Gitea Registry
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
@@ -24,12 +27,12 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Login to GHCR - name: Login to Gitea Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: gitea.netrunner-vpn.com
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GHCR_TOKEN }} password: ${{ secrets.GT_REGISTRY_TOKEN }}
# Сборка Лендинга (Next.js) # Сборка Лендинга (Next.js)
- name: Build and Push Landing - name: Build and Push Landing
@@ -38,9 +41,9 @@ jobs:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
push: true push: true
tags: ghcr.io/nineap/netrunner-landing:latest tags: gitea.netrunner-vpn.com/nineap/netrunner-landing:latest
cache-from: type=registry,ref=ghcr.io/nineap/netrunner-landing:cache cache-from: type=registry,ref=gitea.netrunner-vpn.com/nineap/netrunner-landing:cache
cache-to: type=registry,ref=ghcr.io/nineap/netrunner-landing:cache,mode=max cache-to: type=registry,ref=gitea.netrunner-vpn.com/nineap/netrunner-landing:cache,mode=max
# Сборка Прокси (Rust) # Сборка Прокси (Rust)
- name: Build and Push Proxy - name: Build and Push Proxy
@@ -49,6 +52,6 @@ jobs:
context: ./proxy-ws context: ./proxy-ws
file: ./proxy-ws/Dockerfile file: ./proxy-ws/Dockerfile
push: true push: true
tags: ghcr.io/nineap/netrunner-landing-proxy:latest tags: gitea.netrunner-vpn.com/nineap/netrunner-landing-proxy:latest
cache-from: type=registry,ref=ghcr.io/nineap/netrunner-landing-proxy:cache cache-from: type=registry,ref=gitea.netrunner-vpn.com/nineap/netrunner-landing-proxy:cache
cache-to: type=registry,ref=ghcr.io/nineap/netrunner-landing-proxy:cache,mode=max cache-to: type=registry,ref=gitea.netrunner-vpn.com/nineap/netrunner-landing-proxy:cache,mode=max
+68
View File
@@ -0,0 +1,68 @@
name: Deploy Nginx
# Единственный публичный вход (443/80) на прод VPS (backend+лендинг вместе,
# см. deploy-nginx.yml у netrunner-backend — тот же сервер, свой домен).
# Только ручной запуск.
#
# Порядок первого запуска:
# 1. Запустить эту джобу (поднимет http-конфиг).
# 2. Один раз руками на сервере:
# sudo apt-get install -y certbot
# sudo certbot certonly --webroot -w /var/www/certbot \
# -d netrunner-vpn.com
# 3. Запустить эту джобу ещё раз — включит HTTPS.
on:
workflow_dispatch:
jobs:
deploy:
name: Deploy nginx config for netrunner-vpn.com
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Sync nginx configs to VPS
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ vars.VPS_IP }}
username: root
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "nginx/landing-http.conf,nginx/landing-ssl.conf"
target: "/root/netrunner-frontend"
strip_components: 0
- name: Install/enable nginx via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ vars.VPS_IP }}
username: root
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
set -e
if ! command -v nginx >/dev/null 2>&1; then
apt-get update
apt-get install -y nginx
fi
mkdir -p /var/www/certbot
rm -f /etc/nginx/sites-enabled/default
cp /root/netrunner-frontend/nginx/landing-http.conf /etc/nginx/sites-available/landing-http.conf
cp /root/netrunner-frontend/nginx/landing-ssl.conf /etc/nginx/sites-available/landing-ssl.conf
ln -sf /etc/nginx/sites-available/landing-http.conf /etc/nginx/sites-enabled/landing-http.conf
CERT=/etc/letsencrypt/live/netrunner-vpn.com/fullchain.pem
if [ -f "$CERT" ]; then
ln -sf /etc/nginx/sites-available/landing-ssl.conf /etc/nginx/sites-enabled/landing-ssl.conf
echo "🔒 Сертификат найден — HTTPS-конфиг включён."
else
rm -f /etc/nginx/sites-enabled/landing-ssl.conf
echo "⚠️ Сертификата ещё нет ($CERT) — HTTPS-конфиг НЕ включён."
fi
nginx -t
systemctl enable --now nginx
systemctl reload nginx
echo "✅ nginx (netrunner-vpn.com) synchronized."
+21 -10
View File
@@ -1,6 +1,6 @@
# Копия .github/workflows/deploy.yml для Gitea Actions. # Копия .github/workflows/deploy.yml для Gitea Actions.
# - secrets.GHCR_TOKEN вместо secrets.GITHUB_TOKEN для логина на ghcr.io — # - Образы тянутся из встроенного Container Registry самой Gitea
# автотокен Gitea валиден только для самого Gitea, не для ghcr.io. # (gitea.netrunner-vpn.com), не ghcr.io — secrets.GT_REGISTRY_TOKEN.
# - Триггер workflow_run (авто-деплой после успешной сборки) поддерживается # - Триггер workflow_run (авто-деплой после успешной сборки) поддерживается
# не во всех версиях Gitea Actions — если deploy-dev не срабатывает # не во всех версиях Gitea Actions — если deploy-dev не срабатывает
# автоматически после build, проверить версию Gitea или временно дергать # автоматически после build, проверить версию Gitea или временно дергать
@@ -27,29 +27,38 @@ jobs:
with: with:
ref: ${{ github.event.workflow_run.head_sha }} ref: ${{ github.event.workflow_run.head_sha }}
# Копируем только compose-файл — сам образ дев-сервер тянет из GHCR. # Копируем только compose-файл — сам образ дев-сервер тянет из Gitea Registry.
- name: Sync compose file to Dev VPS - name: Sync compose file to Dev VPS
uses: appleboy/scp-action@v0.1.7 uses: appleboy/scp-action@v0.1.7
with: with:
host: ${{ secrets.DEV_VPS_IP }} host: ${{ vars.DEV_VPS_IP }}
username: root username: root
key: ${{ secrets.DEV_SSH_PRIVATE_KEY }} key: ${{ secrets.DEV_SSH_PRIVATE_KEY }}
source: "docker-compose.dev-remote.yml" source: "docker-compose.dev-remote.yml"
target: "/root/netrunner-frontend-dev" target: "/root/netrunner-frontend-dev"
strip_components: 0 strip_components: 0
- name: Remote deploy via SSH - name: Write .env and deploy via SSH
uses: appleboy/ssh-action@v1 uses: appleboy/ssh-action@v1
with: with:
host: ${{ secrets.DEV_VPS_IP }} host: ${{ vars.DEV_VPS_IP }}
username: root username: root
key: ${{ secrets.DEV_SSH_PRIVATE_KEY }} key: ${{ secrets.DEV_SSH_PRIVATE_KEY }}
envs: "BOT_USERNAME"
script: | script: |
set -e set -e
cd /root/netrunner-frontend-dev cd /root/netrunner-frontend-dev
# Единственное, что этому compose-файлу нужно из .env — свой dev-бот
# (см. BOT_USERNAME:? в docker-compose.dev-remote.yml). Тот же бот,
# что и у netrunner-backend dev — vars.DEV_BOT_USERNAME там же.
cat > .env <<EOF
BOT_USERNAME=${BOT_USERNAME}
EOF
chmod 600 .env
echo "🚀 Deploying landing+proxy:latest to DEV" echo "🚀 Deploying landing+proxy:latest to DEV"
echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin echo "${{ secrets.GT_REGISTRY_TOKEN }}" | docker login gitea.netrunner-vpn.com -u ${{ github.actor }} --password-stdin
docker compose -f docker-compose.dev-remote.yml pull docker compose -f docker-compose.dev-remote.yml pull
docker compose -f docker-compose.dev-remote.yml up -d --remove-orphans docker compose -f docker-compose.dev-remote.yml up -d --remove-orphans
@@ -57,6 +66,8 @@ jobs:
docker image prune -f docker image prune -f
echo "✅ Dev synchronized." echo "✅ Dev synchronized."
env:
BOT_USERNAME: ${{ vars.DEV_BOT_USERNAME }}
deploy-prod: deploy-prod:
name: Pull & restart on Prod VPS name: Pull & restart on Prod VPS
@@ -69,7 +80,7 @@ jobs:
- name: Copy docker-compose.yml to VPS - name: Copy docker-compose.yml to VPS
uses: appleboy/scp-action@v0.1.7 uses: appleboy/scp-action@v0.1.7
with: with:
host: ${{ secrets.VPS_IP }} host: ${{ vars.VPS_IP }}
username: root username: root
key: ${{ secrets.SSH_PRIVATE_KEY }} key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "docker-compose.yml" source: "docker-compose.yml"
@@ -78,13 +89,13 @@ jobs:
- name: Deploy via SSH - name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.3 uses: appleboy/ssh-action@v1.0.3
with: with:
host: ${{ secrets.VPS_IP }} host: ${{ vars.VPS_IP }}
username: root username: root
key: ${{ secrets.SSH_PRIVATE_KEY }} key: ${{ secrets.SSH_PRIVATE_KEY }}
script: | script: |
set -e set -e
cd /root/netrunner-frontend cd /root/netrunner-frontend
echo ${{ secrets.GHCR_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin echo ${{ secrets.GT_REGISTRY_TOKEN }} | docker login gitea.netrunner-vpn.com -u ${{ github.actor }} --password-stdin
echo "🚀 Deploying landing+proxy:latest to PROD" echo "🚀 Deploying landing+proxy:latest to PROD"
+68
View File
@@ -0,0 +1,68 @@
name: Deploy Nginx
# Единственный публичный вход (443/80) на прод VPS (backend+лендинг вместе,
# см. deploy-nginx.yml у netrunner-backend — тот же сервер, свой домен).
# Только ручной запуск.
#
# Порядок первого запуска:
# 1. Запустить эту джобу (поднимет http-конфиг).
# 2. Один раз руками на сервере:
# sudo apt-get install -y certbot
# sudo certbot certonly --webroot -w /var/www/certbot \
# -d netrunner-vpn.com
# 3. Запустить эту джобу ещё раз — включит HTTPS.
on:
workflow_dispatch:
jobs:
deploy:
name: Deploy nginx config for netrunner-vpn.com
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Sync nginx configs to VPS
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ vars.VPS_IP }}
username: root
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "nginx/landing-http.conf,nginx/landing-ssl.conf"
target: "/root/netrunner-frontend"
strip_components: 0
- name: Install/enable nginx via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ vars.VPS_IP }}
username: root
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
set -e
if ! command -v nginx >/dev/null 2>&1; then
apt-get update
apt-get install -y nginx
fi
mkdir -p /var/www/certbot
rm -f /etc/nginx/sites-enabled/default
cp /root/netrunner-frontend/nginx/landing-http.conf /etc/nginx/sites-available/landing-http.conf
cp /root/netrunner-frontend/nginx/landing-ssl.conf /etc/nginx/sites-available/landing-ssl.conf
ln -sf /etc/nginx/sites-available/landing-http.conf /etc/nginx/sites-enabled/landing-http.conf
CERT=/etc/letsencrypt/live/netrunner-vpn.com/fullchain.pem
if [ -f "$CERT" ]; then
ln -sf /etc/nginx/sites-available/landing-ssl.conf /etc/nginx/sites-enabled/landing-ssl.conf
echo "🔒 Сертификат найден — HTTPS-конфиг включён."
else
rm -f /etc/nginx/sites-enabled/landing-ssl.conf
echo "⚠️ Сертификата ещё нет ($CERT) — HTTPS-конфиг НЕ включён."
fi
nginx -t
systemctl enable --now nginx
systemctl reload nginx
echo "✅ nginx (netrunner-vpn.com) synchronized."
+16 -5
View File
@@ -24,23 +24,32 @@ jobs:
- name: Sync compose file to Dev VPS - name: Sync compose file to Dev VPS
uses: appleboy/scp-action@v0.1.7 uses: appleboy/scp-action@v0.1.7
with: with:
host: ${{ secrets.DEV_VPS_IP }} host: ${{ vars.DEV_VPS_IP }}
username: root username: root
key: ${{ secrets.DEV_SSH_PRIVATE_KEY }} key: ${{ secrets.DEV_SSH_PRIVATE_KEY }}
source: "docker-compose.dev-remote.yml" source: "docker-compose.dev-remote.yml"
target: "/root/netrunner-frontend-dev" target: "/root/netrunner-frontend-dev"
strip_components: 0 strip_components: 0
- name: Remote deploy via SSH - name: Write .env and deploy via SSH
uses: appleboy/ssh-action@v1 uses: appleboy/ssh-action@v1
with: with:
host: ${{ secrets.DEV_VPS_IP }} host: ${{ vars.DEV_VPS_IP }}
username: root username: root
key: ${{ secrets.DEV_SSH_PRIVATE_KEY }} key: ${{ secrets.DEV_SSH_PRIVATE_KEY }}
envs: "BOT_USERNAME"
script: | script: |
set -e set -e
cd /root/netrunner-frontend-dev cd /root/netrunner-frontend-dev
# Единственное, что этому compose-файлу нужно из .env — свой dev-бот
# (см. BOT_USERNAME:? в docker-compose.dev-remote.yml). Тот же бот,
# что и у netrunner-backend dev — vars.DEV_BOT_USERNAME там же.
cat > .env <<EOF
BOT_USERNAME=${BOT_USERNAME}
EOF
chmod 600 .env
echo "🚀 Deploying landing+proxy:latest to DEV" echo "🚀 Deploying landing+proxy:latest to DEV"
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin
@@ -50,6 +59,8 @@ jobs:
docker image prune -f docker image prune -f
echo "✅ Dev synchronized." echo "✅ Dev synchronized."
env:
BOT_USERNAME: ${{ vars.DEV_BOT_USERNAME }}
deploy-prod: deploy-prod:
name: Pull & restart on Prod VPS name: Pull & restart on Prod VPS
@@ -62,7 +73,7 @@ jobs:
- name: Copy docker-compose.yml to VPS - name: Copy docker-compose.yml to VPS
uses: appleboy/scp-action@v0.1.7 uses: appleboy/scp-action@v0.1.7
with: with:
host: ${{ secrets.VPS_IP }} host: ${{ vars.VPS_IP }}
username: root username: root
key: ${{ secrets.SSH_PRIVATE_KEY }} key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "docker-compose.yml" source: "docker-compose.yml"
@@ -71,7 +82,7 @@ jobs:
- name: Deploy via SSH - name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.3 uses: appleboy/ssh-action@v1.0.3
with: with:
host: ${{ secrets.VPS_IP }} host: ${{ vars.VPS_IP }}
username: root username: root
key: ${{ secrets.SSH_PRIVATE_KEY }} key: ${{ secrets.SSH_PRIVATE_KEY }}
script: | script: |
+1
View File
@@ -32,6 +32,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed) # env files (can opt-in for committing if needed)
.env* .env*
!.env.example
# vercel # vercel
.vercel .vercel
+28
View File
@@ -0,0 +1,28 @@
import { NextResponse } from "next/server";
import client from "prom-client";
// Явный Node.js рантайм — prom-client (perf_hooks, process.*) не работает в
// Edge Runtime, дефолтном для route handlers в Next.js.
export const runtime = "nodejs";
// register — модульный синглтон: Next.js держит один и тот же процесс между
// запросами в проде (не пересоздаёт модуль на каждый вызов), поэтому
// collectDefaultMetrics() должен быть вызван ровно один раз за жизнь
// процесса — глобальный флаг предотвращает повторную регистрацию тех же
// метрик при hot-reload в dev-режиме.
declare global {
var __prometheusMetricsInitialized: boolean | undefined;
}
if (!global.__prometheusMetricsInitialized) {
client.collectDefaultMetrics();
global.__prometheusMetricsInitialized = true;
}
export async function GET() {
const metrics = await client.register.metrics();
return new NextResponse(metrics, {
status: 200,
headers: { "Content-Type": client.register.contentType },
});
}
+9 -4
View File
@@ -25,10 +25,13 @@ export async function FAQ({
lang: string; lang: string;
}) { }) {
let faqs: PBFaq[] = []; let faqs: PBFaq[] = [];
// Различаем "PocketBase ответил, но для этого языка коллекции ещё нет" (штатно,
// это и есть fallback на en ниже) от "PocketBase вообще не ответил ни разу" —
// раньше оба случая одинаково показывали "0 protocols found", что вводит в
// заблуждение при реальном сбое инфраструктуры (выглядит как "у нас нет FAQ",
// а не "сейчас не грузится").
let hadFetchError = false;
// CMS-контент (в отличие от статичных dictionaries/*.json) переведён только
// для части языков — коллекции `faqs_<lang>` для новых языков ещё не заведены
// в PocketBase. Пока для них нет перевода, показываем английский, а не пустоту.
for (const fallbackLang of Array.from(new Set([lang, "en"]))) { for (const fallbackLang of Array.from(new Set([lang, "en"]))) {
try { try {
faqs = await pb.collection(`faqs_${fallbackLang}`).getFullList<PBFaq>({ faqs = await pb.collection(`faqs_${fallbackLang}`).getFullList<PBFaq>({
@@ -36,8 +39,10 @@ export async function FAQ({
fetch: (url, config) => fetch: (url, config) =>
fetch(url, { ...config, next: { revalidate: 86400 } }), // Кэшируем на сутки fetch(url, { ...config, next: { revalidate: 86400 } }), // Кэшируем на сутки
}); });
hadFetchError = false;
break; break;
} catch (error) { } catch (error) {
hadFetchError = true;
console.error(`Ошибка загрузки FAQ (${fallbackLang}):`, error); console.error(`Ошибка загрузки FAQ (${fallbackLang}):`, error);
} }
} }
@@ -88,7 +93,7 @@ export async function FAQ({
{faqs.length === 0 ? ( {faqs.length === 0 ? (
<div className="text-center py-20 border border-dashed border-border/50 rounded-[2.5rem] bg-card/20"> <div className="text-center py-20 border border-dashed border-border/50 rounded-[2.5rem] bg-card/20">
<p className="text-muted-foreground font-mono text-sm uppercase tracking-widest"> <p className="text-muted-foreground font-mono text-sm uppercase tracking-widest">
0 protocols found in cache. {hadFetchError ? dict.loadError : "0 protocols found in cache."}
</p> </p>
</div> </div>
) : ( ) : (
+36 -1
View File
@@ -63,9 +63,14 @@ export function Pricing({
const [period, setPeriod] = useState<Period>("1y"); const [period, setPeriod] = useState<Period>("1y");
const [plans, setPlans] = useState<PBPricingPlan[]>([]); const [plans, setPlans] = useState<PBPricingPlan[]>([]);
const [loading, setLoading] = useState(!!lang); const [loading, setLoading] = useState(!!lang);
// PocketBase недоступен целиком (не просто нет перевода для этого языка) —
// показываем последний успешно закэшированный список вместо пустой секции.
const [usingCache, setUsingCache] = useState(false);
useEffect(() => { useEffect(() => {
if (!lang) return; if (!lang) return;
const cacheKey = `netrunner_pricing_cache_${lang}`;
async function fetchPlans() { async function fetchPlans() {
try { try {
setLoading(true); setLoading(true);
@@ -74,6 +79,7 @@ export function Pricing({
// `pricing_<lang>` для новых языков ещё не заведена в PocketBase. // `pricing_<lang>` для новых языков ещё не заведена в PocketBase.
// Пока для них нет перевода, показываем английские тарифы, а не пустоту. // Пока для них нет перевода, показываем английские тарифы, а не пустоту.
let records: PBPricingPlan[] = []; let records: PBPricingPlan[] = [];
let allFailed = true;
for (const fallbackLang of Array.from(new Set([lang, "en"]))) { for (const fallbackLang of Array.from(new Set([lang, "en"]))) {
try { try {
records = await pb records = await pb
@@ -82,11 +88,34 @@ export function Pricing({
sort: "order", sort: "order",
requestKey: null, requestKey: null,
}); });
allFailed = false;
break; break;
} catch (error) { } catch (error) {
console.error(`Ошибка загрузки тарифов (${fallbackLang}):`, error); console.error(`Ошибка загрузки тарифов (${fallbackLang}):`, error);
} }
} }
if (allFailed) {
try {
const cached = localStorage.getItem(cacheKey);
if (cached) {
records = JSON.parse(cached);
setUsingCache(true);
}
} catch {
// localStorage недоступен (приватный режим и т.п.) — просто нет кеша.
}
} else {
setUsingCache(false);
if (records.length > 0) {
try {
localStorage.setItem(cacheKey, JSON.stringify(records));
} catch {
// Диск/квота localStorage — не критично, просто не кешируем.
}
}
}
setPlans(records); setPlans(records);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -121,10 +150,16 @@ export function Pricing({
> >
{dict.badge} {dict.badge}
</Badge> </Badge>
<h2 className="text-4xl md:text-5xl font-bold tracking-tighter mb-8"> <h2 className="text-4xl md:text-5xl font-bold tracking-tighter mb-4">
{dict.title} {dict.title}
</h2> </h2>
{usingCache && (
<p className="mb-4 text-xs font-mono uppercase tracking-widest text-muted-foreground/70">
{dict.offlineNotice}
</p>
)}
<div className="inline-flex items-center justify-center p-1.5 bg-card/50 border border-border/50 rounded-2xl backdrop-blur-sm relative"> <div className="inline-flex items-center justify-center p-1.5 bg-card/50 border border-border/50 rounded-2xl backdrop-blur-sm relative">
{periods.map((p: PeriodOption) => ( {periods.map((p: PeriodOption) => (
<button <button
+4 -2
View File
@@ -59,11 +59,13 @@
"billedOnce": "يُدفع دفعة واحدة:", "billedOnce": "يُدفع دفعة واحدة:",
"period1m": "شهر واحد", "period1m": "شهر واحد",
"period6m": "6 أشهر", "period6m": "6 أشهر",
"period1y": "سنة واحدة" "period1y": "سنة واحدة",
"offlineNotice": "يتم عرض آخر أسعار محفوظة — لا يتوفر اتصال بالخادم الآن."
}, },
"faq": { "faq": {
"title": "استعلامات النظام (الأسئلة الشائعة)", "title": "استعلامات النظام (الأسئلة الشائعة)",
"empty": "لم يتم العثور على سجلات." "empty": "لم يتم العثور على سجلات.",
"loadError": "فشل التحميل — حاول تحديث الصفحة."
}, },
"blog": { "blog": {
"badge": "سجلات البيانات", "badge": "سجلات البيانات",
+4 -2
View File
@@ -59,11 +59,13 @@
"billedOnce": "Спішацца адразу:", "billedOnce": "Спішацца адразу:",
"period1m": "1 месяц", "period1m": "1 месяц",
"period6m": "6 месяцаў", "period6m": "6 месяцаў",
"period1y": "1 год" "period1y": "1 год",
"offlineNotice": "Паказаны апошнія захаваныя тарыфы — зараз няма сувязі з серверам."
}, },
"faq": { "faq": {
"title": "Сістэмныя запыты (FAQ)", "title": "Сістэмныя запыты (FAQ)",
"empty": "База ведаў пустая." "empty": "База ведаў пустая.",
"loadError": "Не ўдалося загрузіць — паспрабуйце абнавіць старонку."
}, },
"blog": { "blog": {
"badge": "Data Logs", "badge": "Data Logs",
+4 -2
View File
@@ -59,11 +59,13 @@
"billedOnce": "Billed once:", "billedOnce": "Billed once:",
"period1m": "1 month", "period1m": "1 month",
"period6m": "6 months", "period6m": "6 months",
"period1y": "1 year" "period1y": "1 year",
"offlineNotice": "Showing last saved prices — live data unavailable right now."
}, },
"faq": { "faq": {
"title": "System Queries (FAQ)", "title": "System Queries (FAQ)",
"empty": "No records found." "empty": "No records found.",
"loadError": "Failed to load — try refreshing the page."
}, },
"blog": { "blog": {
"badge": "Data Logs", "badge": "Data Logs",
+4 -2
View File
@@ -59,11 +59,13 @@
"billedOnce": "پرداخت یک‌باره:", "billedOnce": "پرداخت یک‌باره:",
"period1m": "۱ ماه", "period1m": "۱ ماه",
"period6m": "۶ ماه", "period6m": "۶ ماه",
"period1y": "۱ سال" "period1y": "۱ سال",
"offlineNotice": "قیمت‌های ذخیره‌شده قبلی نمایش داده می‌شود — اتصال به سرور اکنون برقرار نیست."
}, },
"faq": { "faq": {
"title": "پرسش‌های سیستم (سوالات متداول)", "title": "پرسش‌های سیستم (سوالات متداول)",
"empty": "هیچ رکوردی یافت نشد." "empty": "هیچ رکوردی یافت نشد.",
"loadError": "بارگذاری ناموفق بود — صفحه را دوباره بارگذاری کنید."
}, },
"blog": { "blog": {
"badge": "لاگ‌های داده", "badge": "لاگ‌های داده",
+4 -2
View File
@@ -59,11 +59,13 @@
"billedOnce": "Спишется разом:", "billedOnce": "Спишется разом:",
"period1m": "1 месяц", "period1m": "1 месяц",
"period6m": "6 месяцев", "period6m": "6 месяцев",
"period1y": "1 год" "period1y": "1 год",
"offlineNotice": "Показаны последние сохранённые тарифы — сейчас нет связи с сервером."
}, },
"faq": { "faq": {
"title": "Системные запросы (FAQ)", "title": "Системные запросы (FAQ)",
"empty": "База знаний пуста." "empty": "База знаний пуста.",
"loadError": "Не удалось загрузить — попробуйте обновить страницу."
}, },
"blog": { "blog": {
"badge": "Data Logs", "badge": "Data Logs",
+4 -2
View File
@@ -59,11 +59,13 @@
"billedOnce": "Tek seferlik ödeme:", "billedOnce": "Tek seferlik ödeme:",
"period1m": "1 ay", "period1m": "1 ay",
"period6m": "6 ay", "period6m": "6 ay",
"period1y": "1 yıl" "period1y": "1 yıl",
"offlineNotice": "Son kaydedilen fiyatlar gösteriliyor — şu anda sunucuya bağlanılamıyor."
}, },
"faq": { "faq": {
"title": "Sistem Sorguları (SSS)", "title": "Sistem Sorguları (SSS)",
"empty": "Kayıt bulunamadı." "empty": "Kayıt bulunamadı.",
"loadError": "Yüklenemedi — sayfayı yenilemeyi deneyin."
}, },
"blog": { "blog": {
"badge": "Veri Kayıtları", "badge": "Veri Kayıtları",
+4 -2
View File
@@ -59,11 +59,13 @@
"billedOnce": "Спишеться одразу:", "billedOnce": "Спишеться одразу:",
"period1m": "1 місяць", "period1m": "1 місяць",
"period6m": "6 місяців", "period6m": "6 місяців",
"period1y": "1 рік" "period1y": "1 рік",
"offlineNotice": "Показано останні збережені тарифи — зараз немає зв'язку із сервером."
}, },
"faq": { "faq": {
"title": "Системні запити (FAQ)", "title": "Системні запити (FAQ)",
"empty": "База знань порожня." "empty": "База знань порожня.",
"loadError": "Не вдалося завантажити — спробуйте оновити сторінку."
}, },
"blog": { "blog": {
"badge": "Data Logs", "badge": "Data Logs",
+4 -2
View File
@@ -59,11 +59,13 @@
"billedOnce": "Bir martalik to'lov:", "billedOnce": "Bir martalik to'lov:",
"period1m": "1 oy", "period1m": "1 oy",
"period6m": "6 oy", "period6m": "6 oy",
"period1y": "1 yil" "period1y": "1 yil",
"offlineNotice": "Oxirgi saqlangan narxlar ko'rsatilmoqda — hozir serverga ulanib bo'lmayapti."
}, },
"faq": { "faq": {
"title": "Tizim So'rovlari (Savollar)", "title": "Tizim So'rovlari (Savollar)",
"empty": "Yozuvlar topilmadi." "empty": "Yozuvlar topilmadi.",
"loadError": "Yuklab bo'lmadi — sahifani yangilashga urinib ko'ring."
}, },
"blog": { "blog": {
"badge": "Ma'lumotlar Jurnali", "badge": "Ma'lumotlar Jurnali",
+4 -2
View File
@@ -59,11 +59,13 @@
"billedOnce": "Thanh toán một lần:", "billedOnce": "Thanh toán một lần:",
"period1m": "1 tháng", "period1m": "1 tháng",
"period6m": "6 tháng", "period6m": "6 tháng",
"period1y": "1 năm" "period1y": "1 năm",
"offlineNotice": "Đang hiển thị giá đã lưu gần nhất — hiện không thể kết nối máy chủ."
}, },
"faq": { "faq": {
"title": "Truy Vấn Hệ Thống (Hỏi Đáp)", "title": "Truy Vấn Hệ Thống (Hỏi Đáp)",
"empty": "Không tìm thấy bản ghi nào." "empty": "Không tìm thấy bản ghi nào.",
"loadError": "Không tải được — hãy thử tải lại trang."
}, },
"blog": { "blog": {
"badge": "Nhật Ký Dữ Liệu", "badge": "Nhật Ký Dữ Liệu",
+4 -2
View File
@@ -59,11 +59,13 @@
"billedOnce": "一次性扣款:", "billedOnce": "一次性扣款:",
"period1m": "1 个月", "period1m": "1 个月",
"period6m": "6 个月", "period6m": "6 个月",
"period1y": "1 年" "period1y": "1 年",
"offlineNotice": "显示上次保存的价格——当前无法连接服务器。"
}, },
"faq": { "faq": {
"title": "系统查询(常见问题)", "title": "系统查询(常见问题)",
"empty": "未找到记录。" "empty": "未找到记录。",
"loadError": "加载失败——请尝试刷新页面。"
}, },
"blog": { "blog": {
"badge": "数据日志", "badge": "数据日志",
+2 -2
View File
@@ -12,7 +12,7 @@
services: services:
# Фронтенд (Next.js) # Фронтенд (Next.js)
landing: landing:
image: ${IMAGE:-ghcr.io/nineap/netrunner-landing:latest} image: ${IMAGE:-gitea.netrunner-vpn.com/nineap/netrunner-landing:latest}
container_name: netrunner-landing-dev container_name: netrunner-landing-dev
restart: always restart: always
ports: ports:
@@ -73,7 +73,7 @@ services:
# Прокси-слой (Rust) # Прокси-слой (Rust)
ws-proxy: ws-proxy:
image: ghcr.io/nineap/netrunner-landing-proxy:latest image: gitea.netrunner-vpn.com/nineap/netrunner-landing-proxy:latest
container_name: netrunner-ws-proxy-dev container_name: netrunner-ws-proxy-dev
restart: always restart: always
ports: ports:
+13 -4
View File
@@ -1,11 +1,17 @@
services: services:
# Фронтенд (Next.js) # Фронтенд (Next.js)
landing: landing:
image: ghcr.io/nineap/netrunner-landing:latest image: gitea.netrunner-vpn.com/nineap/netrunner-landing:latest
container_name: netrunner-landing container_name: netrunner-landing
restart: always restart: always
# Не 127.0.0.1 — /api/metrics (см. app/api/metrics/route.ts) должен
# скрейпиться удалённым центральным Prometheus (см.
# netrunner-data/docker-compose.observability.yml). Порт 3000 в целом уже
# не секрет (это тот же самый сайт, что и по HTTPS через реверс-прокси
# перед ним) — но ОБЯЗАТЕЛЬНО зафайрволить на этом VPS на IP
# observability-сервера, чтобы не плодить лишний голый-HTTP путь к сайту.
ports: ports:
- "127.0.0.1:3000:3000" - "3000:3000"
environment: environment:
- NODE_ENV=production - NODE_ENV=production
# PB_PUBLIC_URL — адрес PocketBase для клиентских компонентов (Pricing, # PB_PUBLIC_URL — адрес PocketBase для клиентских компонентов (Pricing,
@@ -48,11 +54,14 @@ services:
# Прокси-слой (Rust) # Прокси-слой (Rust)
ws-proxy: ws-proxy:
image: ghcr.io/nineap/netrunner-landing-proxy:latest image: gitea.netrunner-vpn.com/nineap/netrunner-landing-proxy:latest
container_name: netrunner-ws-proxy container_name: netrunner-ws-proxy
restart: always restart: always
# Не 127.0.0.1 — /metrics (см. proxy-ws/src/main.rs) должен скрейпиться
# удалённым центральным Prometheus. Обязательно зафайрволить на IP
# observability-сервера (см. netrunner-data/docker-compose.observability.yml).
ports: ports:
- "127.0.0.1:3001:3001" - "3001:3001"
environment: environment:
- PB_INTERNAL_URL=http://netrunner-cms:8090 - PB_INTERNAL_URL=http://netrunner-cms:8090
depends_on: depends_on:
+2
View File
@@ -42,10 +42,12 @@ export interface Dictionary {
period1m: string; period1m: string;
period6m: string; period6m: string;
period1y: string; period1y: string;
offlineNotice: string;
}; };
faq: { faq: {
title: string; title: string;
empty: string; empty: string;
loadError: string;
}; };
blog: { blog: {
badge: string; badge: string;
+15
View File
@@ -0,0 +1,15 @@
# Часть 1/2 — включается всегда, с самого первого деплоя (см.
# .gitea/workflows/deploy-nginx.yml). ACME-challenge + редирект на HTTPS.
server {
listen 80;
listen [::]:80;
server_name netrunner-vpn.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
+54
View File
@@ -0,0 +1,54 @@
# Часть 2/2 — деплой включает этот файл ТОЛЬКО когда сертификат уже есть.
# ВАЖНО: сертификат выпускается ОДНОЙ командой на оба домена сразу (см.
# .gitea/workflows/deploy-nginx.yml у netrunner-backend, туда же убегает
# первый -d) — папка в /etc/letsencrypt/live называется по ПЕРВОМУ домену,
# account.netrunner-vpn.com, а не netrunner-vpn.com, даже для этого файла.
#
# Реальный IP посетителя — из CF-Connecting-IP, см.
# /etc/nginx/conf.d/cloudflare-real-ip.conf на самом сервере (общий для всех
# доменов на этом VPS, не часть этого репозитория).
#
# Три бэкенда за одним доменом:
# / -> сам лендинг (Next.js, :3000)
# /cms-api/* -> PocketBase (:8090) — префикс "/cms-api" меняется на
# родной для PocketBase "/api" (см. PB_PUBLIC_URL в
# docker-compose.yml)
# /api/* -> proxy-ws (:3001) — его собственные роуты уже объявлены
# с префиксом /api (comments, speedtest), никакой замены
# префикса не нужно, только вебсокет-апгрейд заголовки
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name netrunner-vpn.com;
ssl_certificate /etc/letsencrypt/live/account.netrunner-vpn.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/account.netrunner-vpn.com/privkey.pem;
location /cms-api/ {
proxy_pass http://127.0.0.1:8090/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/ {
proxy_pass http://127.0.0.1:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
+1
View File
@@ -21,6 +21,7 @@
"next": "16.2.4", "next": "16.2.4",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"pocketbase": "^0.26.8", "pocketbase": "^0.26.8",
"prom-client": "^15.1.3",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
+33 -2
View File
@@ -37,13 +37,16 @@ importers:
version: 1.0.0 version: 1.0.0
next: next:
specifier: 16.2.4 specifier: 16.2.4
version: 16.2.4(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 16.2.4(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
next-themes: next-themes:
specifier: ^0.4.6 specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
pocketbase: pocketbase:
specifier: ^0.26.8 specifier: ^0.26.8
version: 0.26.8 version: 0.26.8
prom-client:
specifier: ^15.1.3
version: 15.1.3
radix-ui: radix-ui:
specifier: ^1.4.3 specifier: ^1.4.3
version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -648,6 +651,10 @@ packages:
'@open-draft/until@2.1.0': '@open-draft/until@2.1.0':
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
'@opentelemetry/api@1.9.1':
resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==}
engines: {node: '>=8.0.0'}
'@radix-ui/number@1.1.1': '@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
@@ -1765,6 +1772,9 @@ packages:
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
hasBin: true hasBin: true
bintrees@1.0.2:
resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
body-parser@2.2.2: body-parser@2.2.2:
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -3199,6 +3209,10 @@ packages:
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
prom-client@15.1.3:
resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==}
engines: {node: ^16 || ^18 || >=20}
prompts@2.4.2: prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -3558,6 +3572,9 @@ packages:
resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==}
engines: {node: '>=6'} engines: {node: '>=6'}
tdigest@0.1.2:
resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
tiny-invariant@1.3.3: tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@@ -4356,6 +4373,8 @@ snapshots:
'@open-draft/until@2.1.0': {} '@open-draft/until@2.1.0': {}
'@opentelemetry/api@1.9.1': {}
'@radix-ui/number@1.1.1': {} '@radix-ui/number@1.1.1': {}
'@radix-ui/primitive@1.1.3': {} '@radix-ui/primitive@1.1.3': {}
@@ -5507,6 +5526,8 @@ snapshots:
baseline-browser-mapping@2.10.19: {} baseline-browser-mapping@2.10.19: {}
bintrees@1.0.2: {}
body-parser@2.2.2: body-parser@2.2.2:
dependencies: dependencies:
bytes: 3.1.2 bytes: 3.1.2
@@ -6805,7 +6826,7 @@ snapshots:
react: 19.2.4 react: 19.2.4
react-dom: 19.2.4(react@19.2.4) react-dom: 19.2.4(react@19.2.4)
next@16.2.4(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): next@16.2.4(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies: dependencies:
'@next/env': 16.2.4 '@next/env': 16.2.4
'@swc/helpers': 0.5.15 '@swc/helpers': 0.5.15
@@ -6824,6 +6845,7 @@ snapshots:
'@next/swc-linux-x64-musl': 16.2.4 '@next/swc-linux-x64-musl': 16.2.4
'@next/swc-win32-arm64-msvc': 16.2.4 '@next/swc-win32-arm64-msvc': 16.2.4
'@next/swc-win32-x64-msvc': 16.2.4 '@next/swc-win32-x64-msvc': 16.2.4
'@opentelemetry/api': 1.9.1
sharp: 0.34.5 sharp: 0.34.5
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
@@ -7027,6 +7049,11 @@ snapshots:
dependencies: dependencies:
parse-ms: 4.0.0 parse-ms: 4.0.0
prom-client@15.1.3:
dependencies:
'@opentelemetry/api': 1.9.1
tdigest: 0.1.2
prompts@2.4.2: prompts@2.4.2:
dependencies: dependencies:
kleur: 3.0.3 kleur: 3.0.3
@@ -7548,6 +7575,10 @@ snapshots:
tapable@2.3.2: {} tapable@2.3.2: {}
tdigest@0.1.2:
dependencies:
bintrees: 1.0.2
tiny-invariant@1.3.3: {} tiny-invariant@1.3.3: {}
tinyglobby@0.2.16: tinyglobby@0.2.16:
+372 -12
View File
@@ -137,6 +137,12 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.5.3" version = "0.5.3"
@@ -472,6 +478,17 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "evmap"
version = "11.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8874945f036109c72242964c1174cf99434e30cfa45bf45fedc983f50046f8"
dependencies = [
"hashbag",
"left-right",
"smallvec",
]
[[package]] [[package]]
name = "filetime" name = "filetime"
version = "0.2.27" version = "0.2.27"
@@ -505,6 +522,12 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.2" version = "1.2.2"
@@ -553,6 +576,21 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "generator"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae"
dependencies = [
"cc",
"cfg-if",
"libc",
"log",
"rustversion",
"windows-link",
"windows-result",
]
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.7"
@@ -574,6 +612,18 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.27" version = "0.3.27"
@@ -604,6 +654,12 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "hashbag"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7040a10f52cba493ddb09926e15d10a9d8a28043708a405931fe4c6f19fac064"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.5" version = "0.14.5"
@@ -613,6 +669,15 @@ dependencies = [
"ahash", "ahash",
] ]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"foldhash",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.17.0" version = "0.17.0"
@@ -967,6 +1032,17 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "left-right"
version = "0.11.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f0c21e4c8ff95f487fb34e6f9182875f42c84cef966d29216bf115d9bba835a"
dependencies = [
"crossbeam-utils",
"loom",
"slab",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.186" version = "0.2.186"
@@ -1075,6 +1151,19 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "loom"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
dependencies = [
"cfg-if",
"generator",
"scoped-tls",
"tracing",
"tracing-subscriber",
]
[[package]] [[package]]
name = "macro_rules_attribute" name = "macro_rules_attribute"
version = "0.2.2" version = "0.2.2"
@@ -1097,6 +1186,15 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]] [[package]]
name = "matchit" name = "matchit"
version = "0.7.3" version = "0.7.3"
@@ -1128,6 +1226,48 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "metrics"
version = "0.24.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89550ee9f79e88fef3119de263694973a8adb26c21d75322164fb8c493039fe2"
dependencies = [
"portable-atomic",
"rapidhash",
]
[[package]]
name = "metrics-exporter-prometheus"
version = "0.18.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db0d8f1fc9e62caebd0319e11eaec5822b0186c171568f0480b46a0137f9108"
dependencies = [
"base64 0.22.1",
"evmap",
"indexmap",
"metrics",
"metrics-util",
"quanta",
"thiserror 2.0.18",
]
[[package]]
name = "metrics-util"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96f8722f8562635f92f8ed992f26df0532266eb03d5202607c20c0d7e9745e13"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
"hashbrown 0.16.1",
"metrics",
"quanta",
"rand 0.9.4",
"rand_xoshiro",
"rapidhash",
"sketches-ddsketch",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@@ -1208,6 +1348,15 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "num-complex" name = "num-complex"
version = "0.4.6" version = "0.4.6"
@@ -1459,6 +1608,8 @@ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
"indexmap", "indexmap",
"metrics",
"metrics-exporter-prometheus",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
@@ -1469,6 +1620,21 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "quanta"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7"
dependencies = [
"crossbeam-utils",
"libc",
"once_cell",
"raw-cpuid",
"wasi",
"web-sys",
"winapi",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.45" version = "1.0.45"
@@ -1478,6 +1644,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.6" version = "0.8.6"
@@ -1485,8 +1657,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [ dependencies = [
"libc", "libc",
"rand_chacha", "rand_chacha 0.3.1",
"rand_core", "rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.5",
] ]
[[package]] [[package]]
@@ -1496,7 +1678,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [ dependencies = [
"ppv-lite86", "ppv-lite86",
"rand_core", "rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.5",
] ]
[[package]] [[package]]
@@ -1505,7 +1697,16 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.17",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
] ]
[[package]] [[package]]
@@ -1515,7 +1716,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
dependencies = [ dependencies = [
"num-traits", "num-traits",
"rand", "rand 0.8.6",
]
[[package]]
name = "rand_xoshiro"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41"
dependencies = [
"rand_core 0.9.5",
]
[[package]]
name = "rapidhash"
version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e"
dependencies = [
"rustversion",
]
[[package]]
name = "raw-cpuid"
version = "11.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
dependencies = [
"bitflags 2.11.1",
] ]
[[package]] [[package]]
@@ -1651,7 +1879,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"getrandom", "getrandom 0.2.17",
"libc", "libc",
"untrusted", "untrusted",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@@ -1745,6 +1973,12 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@@ -1838,6 +2072,15 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@@ -1860,6 +2103,12 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "sketches-ddsketch"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.12" version = "0.4.12"
@@ -2022,7 +2271,16 @@ version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl 2.0.18",
] ]
[[package]] [[package]]
@@ -2036,6 +2294,26 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.41" version = "0.3.41"
@@ -2101,7 +2379,7 @@ dependencies = [
"aho-corasick", "aho-corasick",
"derive_builder", "derive_builder",
"esaxx-rs", "esaxx-rs",
"getrandom", "getrandom 0.2.17",
"indicatif", "indicatif",
"itertools 0.12.1", "itertools 0.12.1",
"lazy_static", "lazy_static",
@@ -2110,7 +2388,7 @@ dependencies = [
"monostate", "monostate",
"onig", "onig",
"paste", "paste",
"rand", "rand 0.8.6",
"rayon", "rayon",
"rayon-cond", "rayon-cond",
"regex", "regex",
@@ -2118,7 +2396,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"spm_precompiled", "spm_precompiled",
"thiserror", "thiserror 1.0.69",
"unicode-normalization-alignments", "unicode-normalization-alignments",
"unicode-segmentation", "unicode-segmentation",
"unicode_categories", "unicode_categories",
@@ -2237,6 +2515,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
] ]
[[package]] [[package]]
@@ -2368,9 +2676,9 @@ version = "0.21.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "522b7a8131564765af8fa623ab262d6e6b650bc95663d805edca1e9ee94d6ec6" checksum = "522b7a8131564765af8fa623ab262d6e6b650bc95663d805edca1e9ee94d6ec6"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.17",
"log", "log",
"rand", "rand 0.8.6",
"rand_distr", "rand_distr",
"rustfft", "rustfft",
"tract-nnef", "tract-nnef",
@@ -2470,6 +2778,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"
@@ -2501,6 +2815,15 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.4+wasi-0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
dependencies = [
"wit-bindgen",
]
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.120" version = "0.2.120"
@@ -2582,6 +2905,22 @@ version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.11" version = "0.1.11"
@@ -2591,12 +2930,27 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"
@@ -2764,6 +3118,12 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]] [[package]]
name = "writeable" name = "writeable"
version = "0.6.3" version = "0.6.3"
+2
View File
@@ -15,6 +15,8 @@ tower-http = { version = "0.5", features = ["cors"] }
tract-onnx = "0.21.6" tract-onnx = "0.21.6"
tokenizers = "0.19" tokenizers = "0.19"
anyhow = "1.0" anyhow = "1.0"
metrics = "0.24.3"
metrics-exporter-prometheus = { version = "0.18.1", default-features = false }
# Профиль заточен под минимальный размер бинарника и образа (LTO, strip # Профиль заточен под минимальный размер бинарника и образа (LTO, strip
# отладочной информации, отказ от unwind-раскрутки стека при панике). # отладочной информации, отказ от unwind-раскрутки стека при панике).
+42 -6
View File
@@ -1,14 +1,17 @@
use axum::{ use axum::{
body::Bytes, body::Bytes,
extract::{DefaultBodyLimit, State}, extract::{DefaultBodyLimit, MatchedPath, Request, State},
http::{header, StatusCode}, http::{header, StatusCode},
response::IntoResponse, middleware::{self, Next},
response::{IntoResponse, Response},
routing::{get, post}, routing::{get, post},
Json, Router, Json, Router,
}; };
use metrics_exporter_prometheus::PrometheusBuilder;
use reqwest::Client; use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use std::time::Instant;
mod classifier; mod classifier;
use classifier::ToxicityClassifier; use classifier::ToxicityClassifier;
@@ -64,13 +67,25 @@ async fn main() {
ai_classifier: classifier, ai_classifier: classifier,
}); });
// Для центрального Prometheus (см. netrunner-data/docker-compose.observability.yml,
// scrape по сети — этот порт нужно будет зафайрволить на его IP так же,
// как /metrics на прокси-нодах).
let recorder_handle = PrometheusBuilder::new()
.install_recorder()
.expect("Failed to install Prometheus recorder");
let app = Router::new() let app = Router::new()
.route("/api/comments", post(create_comment)) .route("/api/comments", post(create_comment))
.route("/api/speedtest/ping", get(speedtest_ping)) .route("/api/speedtest/ping", get(speedtest_ping))
.route("/api/speedtest/download", get(speedtest_download)) .route("/api/speedtest/download", get(speedtest_download))
.route("/api/speedtest/upload", post(speedtest_upload)) .route("/api/speedtest/upload", post(speedtest_upload))
.layer(DefaultBodyLimit::max(100 * 1024 * 1024)) .layer(DefaultBodyLimit::max(100 * 1024 * 1024))
.with_state(state); .with_state(state)
.route(
"/metrics",
get(move || std::future::ready(recorder_handle.render())),
)
.route_layer(middleware::from_fn(track_metrics));
println!("🚀 [READY] Netrunner Gateway started on 0.0.0.0:3001"); println!("🚀 [READY] Netrunner Gateway started on 0.0.0.0:3001");
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap(); let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
@@ -191,9 +206,7 @@ async fn speedtest_ping() -> impl IntoResponse {
[ [
( (
header::CACHE_CONTROL, header::CACHE_CONTROL,
header::HeaderValue::from_static( header::HeaderValue::from_static("no-store, no-cache, must-revalidate, max-age=0"),
"no-store, no-cache, must-revalidate, max-age=0",
),
), ),
(header::PRAGMA, header::HeaderValue::from_static("no-cache")), (header::PRAGMA, header::HeaderValue::from_static("no-cache")),
], ],
@@ -226,3 +239,26 @@ async fn speedtest_upload(body: Bytes) -> impl IntoResponse {
println!("📊 [SPEEDTEST] Upload received: {} bytes", body.len()); println!("📊 [SPEEDTEST] Upload received: {} bytes", body.len());
(StatusCode::OK, format!("Received {} bytes", body.len())) (StatusCode::OK, format!("Received {} bytes", body.len()))
} }
/// Тот же паттерн, что `api.rs::track_metrics` в netrunner-backend —
/// латентность/число запросов по маршруту+методу+статусу.
async fn track_metrics(req: Request, next: Next) -> Response {
let start = Instant::now();
let path = req
.extensions()
.get::<MatchedPath>()
.map(|m| m.as_str().to_owned())
.unwrap_or_else(|| req.uri().path().to_owned());
let method = req.method().to_string();
let response = next.run(req).await;
let latency = start.elapsed().as_secs_f64();
let status = response.status().as_u16().to_string();
let labels = [("method", method), ("path", path), ("status", status)];
metrics::counter!("http_requests_total", &labels).increment(1);
metrics::histogram!("http_request_duration_seconds", &labels).record(latency);
response
}