{#if title}
{title}
{/if}
diff --git a/bot/app/web/frontend/src/lib/webapp/mockApi.js b/bot/app/web/frontend/src/lib/webapp/mockApi.js
index c73872c..b131521 100644
--- a/bot/app/web/frontend/src/lib/webapp/mockApi.js
+++ b/bot/app/web/frontend/src/lib/webapp/mockApi.js
@@ -66,11 +66,32 @@ export async function mockApi(path, options = {}, context = {}) {
premium_traffic: { state: "none" },
},
];
+ const mockAdminDailySeries = (() => {
+ const days = 730;
+ const out = [];
+ const now = new Date();
+ for (let i = 0; i < days; i++) {
+ const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
+ d.setUTCDate(d.getUTCDate() - (days - 1 - i));
+ const iso = d.toISOString().slice(0, 10);
+ const wave = Math.sin(i / 5) * 520 + 720 + ((i * 41) % 280);
+ out.push({ date: iso, amount: Math.max(0, Math.round(wave)) });
+ }
+ return out;
+ })();
if (path === "/admin/stats") {
return {
ok: true,
+ currency_symbol: "RUB",
users: { total_users: 248, active_subscriptions: 172, banned_users: 3 },
- financial: { total_revenue: 186240, successful_payments_count: 934 },
+ financial: {
+ today_revenue: 1240,
+ week_revenue: 15800,
+ month_revenue: 44100,
+ all_time_revenue: 186240,
+ today_payments_count: 4,
+ daily_series: mockAdminDailySeries,
+ },
panel_sync: {
status: "success",
last_sync_time: new Date().toISOString(),
diff --git a/bot/app/web/frontend/src/styles/admin.css b/bot/app/web/frontend/src/styles/admin.css
index 17dbd99..7790901 100644
--- a/bot/app/web/frontend/src/styles/admin.css
+++ b/bot/app/web/frontend/src/styles/admin.css
@@ -17,9 +17,7 @@
inset: 0;
width: 100vw;
height: 100dvh;
- background:
- radial-gradient(circle at 18% 0%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 32%),
- #02070b;
+ background: #02070b;
color: var(--admin-text);
overflow: hidden;
overscroll-behavior: contain;
@@ -313,6 +311,15 @@
flex-shrink: 0;
}
+.admin-section-stage {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ min-width: 0;
+ flex-shrink: 0;
+ width: 100%;
+}
+
.admin-card {
border: 1px solid var(--admin-border);
background: var(--admin-card-bg);
@@ -551,6 +558,185 @@
min-width: 0;
}
+.admin-revenue-chart-head {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px 14px;
+ min-width: 0;
+}
+
+.admin-revenue-chart-toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 8px;
+ min-width: 0;
+}
+
+.admin-revenue-chart-toolbar .admin-revenue-period {
+ flex: 1 1 auto;
+ justify-content: flex-end;
+}
+
+.admin-revenue-granularity {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ align-items: center;
+}
+
+.admin-revenue-period-btn--compact {
+ padding: 4px 9px;
+ font-size: 10px;
+}
+
+.admin-revenue-period-btn:disabled {
+ opacity: 0.45;
+ cursor: not-allowed;
+}
+
+.admin-revenue-range-popover {
+ z-index: 80;
+ width: min(100vw - 24px, 320px);
+ padding: 12px 14px 14px;
+ border-radius: 12px;
+ border: 1px solid var(--admin-border);
+ background: var(--admin-surface);
+ color: var(--admin-text);
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
+}
+
+.admin-revenue-range-popover__title {
+ font-size: 12px;
+ font-weight: 600;
+ margin-bottom: 10px;
+ color: var(--admin-text);
+}
+
+.admin-revenue-range-popover__actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 12px;
+}
+
+.admin-revenue-rcal {
+ margin-top: 2px;
+}
+
+.admin-revenue-rcal__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 8px;
+}
+
+.admin-revenue-rcal__heading {
+ flex: 1;
+ text-align: center;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--admin-text);
+}
+
+.admin-revenue-rcal__nav {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ border: 1px solid var(--admin-border);
+ background: color-mix(in srgb, var(--admin-surface-2) 70%, var(--admin-bg));
+ color: var(--admin-text);
+ cursor: pointer;
+}
+
+.admin-revenue-rcal__nav:hover {
+ background: color-mix(in srgb, var(--admin-surface) 80%, var(--admin-bg));
+}
+
+.admin-revenue-rcal__grids {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.admin-revenue-rcal__grid {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.admin-revenue-rcal__weekrow {
+ display: flex;
+ width: 100%;
+}
+
+.admin-revenue-rcal__headcell {
+ flex: 1;
+ text-align: center;
+ font-size: 10px;
+ font-weight: 500;
+ color: var(--admin-muted);
+ padding: 2px 0 6px;
+ min-width: 0;
+}
+
+.admin-revenue-rcal__cell {
+ flex: 1;
+ padding: 0;
+ text-align: center;
+ min-width: 0;
+}
+
+.admin-revenue-rcal__day {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ max-width: 36px;
+ height: 32px;
+ margin: 0 auto;
+ border-radius: 8px;
+ font-size: 11px;
+ font-weight: 500;
+ color: var(--admin-text);
+ background: transparent;
+ border: 1px solid transparent;
+ cursor: pointer;
+}
+
+.admin-revenue-rcal__day:hover {
+ background: color-mix(in srgb, var(--admin-surface-2) 80%, var(--admin-bg));
+}
+
+.admin-revenue-rcal__day[data-highlighted] {
+ background: color-mix(in srgb, var(--accent) 10%, var(--admin-surface));
+}
+
+.admin-revenue-rcal__day[data-selection-start],
+.admin-revenue-rcal__day[data-selection-end] {
+ background: color-mix(in srgb, var(--accent) 35%, var(--admin-surface));
+ color: var(--admin-text);
+ border-color: color-mix(in srgb, var(--accent) 45%, transparent);
+}
+
+.admin-revenue-rcal__day[data-selected]:not([data-selection-start]):not([data-selection-end]) {
+ background: color-mix(in srgb, var(--accent) 14%, var(--admin-surface));
+}
+
+.admin-revenue-rcal__day[data-disabled] {
+ opacity: 0.35;
+ pointer-events: none;
+}
+
+.admin-revenue-rcal__day[data-outside-month] {
+ opacity: 0.25;
+}
+
.admin-revenue-chart-title {
font-size: 11px;
font-weight: 600;
@@ -559,19 +745,131 @@
color: var(--admin-muted);
}
+.admin-revenue-period {
+ display: inline-flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ padding: 3px;
+ border-radius: 10px;
+ border: 1px solid color-mix(in srgb, var(--admin-border) 80%, transparent);
+ background: color-mix(in srgb, var(--admin-surface-2) 55%, var(--admin-bg));
+}
+
+.admin-revenue-period-btn {
+ appearance: none;
+ border: 0;
+ margin: 0;
+ padding: 6px 10px;
+ border-radius: 7px;
+ font-size: 11px;
+ font-weight: 650;
+ letter-spacing: 0.02em;
+ color: var(--admin-muted);
+ background: transparent;
+ cursor: pointer;
+ font-family: inherit;
+ line-height: 1.1;
+}
+
+.admin-revenue-period-btn:hover {
+ color: var(--admin-text);
+ background: color-mix(in srgb, var(--admin-surface) 55%, transparent);
+}
+
+.admin-revenue-period-btn.is-active {
+ color: color-mix(in srgb, var(--accent) 22%, var(--admin-text));
+ background: color-mix(in srgb, var(--accent) 12%, var(--admin-surface));
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 28%, transparent);
+}
+
+.admin-revenue-chart-hint {
+ margin: 0;
+ font-size: 11px;
+ line-height: 1.35;
+}
+
+.admin-revenue-chart-meta {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ gap: 4px 8px;
+ font-size: 11px;
+ line-height: 1.35;
+}
+
+.admin-revenue-chart-meta-sep {
+ opacity: 0.55;
+}
+
.admin-revenue-svg-frame {
+ position: relative;
+ width: 100%;
+ min-width: 0;
border-radius: 10px;
border: 1px solid var(--admin-border);
background: color-mix(in srgb, var(--admin-bg) 88%, var(--admin-surface-2));
overflow: hidden;
}
-.admin-revenue-svg {
+.admin-revenue-svg-frame--chart {
width: 100%;
- height: 168px;
+}
+
+.admin-revenue-uplot-host {
+ width: 100%;
+ min-width: 0;
display: block;
}
+/* uPlot ships `width: min-content` on `.uplot`, which can prevent full-width layout */
+.admin-revenue-uplot-host .uplot {
+ width: 100%;
+ max-width: 100%;
+ min-width: 0;
+}
+
+.admin-revenue-uplot-host .u-wrap {
+ width: 100%;
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+/* uPlot (canvas) — dark admin theme */
+.admin-uplot.uplot {
+ color: var(--admin-muted);
+ font-family: var(--font-sans, system-ui, sans-serif);
+}
+
+.admin-uplot .u-title {
+ color: var(--admin-text);
+}
+
+.admin-revenue-uplot-host .u-legend {
+ padding: 10px 12px 8px;
+ box-sizing: border-box;
+}
+
+.admin-uplot .u-legend {
+ color: var(--admin-muted);
+ font-size: 11px;
+}
+
+.admin-uplot .u-legend th,
+.admin-uplot .u-legend td {
+ color: var(--admin-text);
+}
+
+.admin-uplot .u-inline {
+ border-color: var(--admin-border);
+}
+
+.admin-revenue-chart-skeleton {
+ display: block;
+ width: 100%;
+ height: min(204px, 36vw);
+ min-height: 120px;
+}
+
.admin-revenue-xlabels {
display: flex;
justify-content: space-between;
diff --git a/bot/app/web/frontend/src/styles/components.css b/bot/app/web/frontend/src/styles/components.css
index 6c394e6..91d8c8c 100644
--- a/bot/app/web/frontend/src/styles/components.css
+++ b/bot/app/web/frontend/src/styles/components.css
@@ -96,6 +96,10 @@
min-height: 132px;
}
+.admin-cn-card-skeleton--tall {
+ min-height: 0;
+}
+
@media (min-width: 900px) {
.admin-cn-card-title {
font-size: 1.65rem;
diff --git a/bot/app/web/frontend/src/styles/dialogs.css b/bot/app/web/frontend/src/styles/dialogs.css
index 8a18761..f3ae77c 100644
--- a/bot/app/web/frontend/src/styles/dialogs.css
+++ b/bot/app/web/frontend/src/styles/dialogs.css
@@ -29,7 +29,6 @@
background: rgba(0, 0, 0, 0.56);
backdrop-filter: blur(10px);
cursor: pointer;
- animation: dialog-fade-in 0.18s ease-out both;
}
.dialog-card {
@@ -47,7 +46,6 @@
background: color-mix(in srgb, var(--panel) 94%, #07111a);
color: var(--text);
box-shadow: 0 26px 70px rgba(0, 0, 0, 0.46);
- animation: dialog-slide-up 0.2s ease-out both;
}
.dialog-head {
@@ -118,22 +116,6 @@
min-height: 0;
}
-@keyframes dialog-fade-in {
- from { opacity: 0; }
- to { opacity: 1; }
-}
-
-@keyframes dialog-slide-up {
- from {
- opacity: 0;
- transform: translateY(18px) scale(0.98);
- }
- to {
- opacity: 1;
- transform: translateY(0) scale(1);
- }
-}
-
@media (min-width: 720px) {
.dialog-card {
border-radius: var(--radius-lg);
diff --git a/db/dal/payment_dal.py b/db/dal/payment_dal.py
index e09bd86..dd00791 100644
--- a/db/dal/payment_dal.py
+++ b/db/dal/payment_dal.py
@@ -251,7 +251,8 @@ async def get_financial_statistics(session: AsyncSession) -> Dict[str, Any]:
today_count = await session.execute(stmt_count_today)
today_payments_count = today_count.scalar() or 0
- daily_series = await _daily_revenue_series_utc(session, days=14)
+ # Longer tail for admin dashboard charts (presets up to 1y + custom range on the client).
+ daily_series = await _daily_revenue_series_utc(session, days=730)
return {
"today_revenue": float(today_amount),
diff --git a/docs/deployment.md b/docs/deployment.md
index 0f84dd9..ddf8956 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -47,14 +47,21 @@ docker compose up -d --build
## Резервная копия и восстановление PostgreSQL
-Имя контейнера БД в типичном Compose — `remnawave-minishop-db`. Подставьте значения **`POSTGRES_USER`** и **`POSTGRES_DB`** из `.env` (на хосте можно выполнить `set -a && source .env && set +a` перед командами или подставить вручную).
+Имя контейнера БД в типичном Compose — `remnawave-minishop-db`. Учётные данные уже передаются в контейнер через `env_file: .env`, поэтому надёжнее вызывать `pg_dump` / `psql` **внутри** контейнера через `sh -c '...'`, чтобы переменные раскрылись там, а не на хосте.
-### Резервная копия (текстовый SQL)
+Если написать на хосте `pg_dump -U "$POSTGRES_USER" ...` без экспорта переменных из `.env`, подставится пустая строка: PostgreSQL тогда берёт имя пользователя ОС (часто `root`) и выдаёт `FATAL: role "root" does not exist`. В **PowerShell** `$POSTGRES_DB` из файла `.env` сам не подхватывается — пустое имя базы даёт у `dropdb` ошибку `missing required argument database name`.
-Логическое дампирование в один файл:
+**Вариант A (рекомендуется):** переменные только внутри контейнера:
```bash
-docker exec remnawave-minishop-db pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > backup.sql
+docker exec remnawave-minishop-db sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB"' > backup.sql
+```
+
+**Вариант B:** сначала загрузить `.env` в текущую сессию на сервере, затем обычная команда (переменные раскроются на хосте):
+
+```bash
+set -a && source .env && set +a
+docker exec remnawave-minishop-db pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" > backup.sql
```
Такой файл удобно хранить вне сервера. При необходимости добавьте к `pg_dump` параметры сжатия или расписание через cron.
@@ -74,18 +81,30 @@ docker compose -f docker-compose-remote-server.yml stop remnawave-minishop
2. Удалите базу и создайте пустую с тем же именем — пользователь из `.env` в официальном образе PostgreSQL обычно суперпользователь и может это сделать:
```bash
-docker exec remnawave-minishop-db dropdb -U "$POSTGRES_USER" "$POSTGRES_DB" --if-exists
-docker exec remnawave-minishop-db createdb -U "$POSTGRES_USER" "$POSTGRES_DB"
+docker exec remnawave-minishop-db sh -c 'dropdb -U "$POSTGRES_USER" --if-exists "$POSTGRES_DB"'
+docker exec remnawave-minishop-db sh -c 'createdb -U "$POSTGRES_USER" "$POSTGRES_DB"'
```
+Имя базы у `dropdb` — последний аргумент; флаг **`--if-exists`** ставьте перед ним (иначе клиент может неверно разобрать командную строку).
+
Если `dropdb` сообщает, что база занята, убедитесь, что остановлен сервис `remnawave-minishop` и к базе нет других подключений.
-3. Восстановите данные из файла на хосте:
+3. Восстановите данные из файла на хосте. Переменные снова должны раскрываться **внутри** контейнера; на стороне `psql` имеет смысл включить **`ON_ERROR_STOP`**, чтобы при первой ошибке в дампе команда завершилась с ненулевым кодом.
+
+**Bash:**
```bash
-docker exec -i remnawave-minishop-db psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" < backup.sql
+docker exec -i remnawave-minishop-db sh -c 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"' < backup.sql
```
+**PowerShell** (перенаправление `<` в `docker exec` часто не подходит; надёжнее передать дамп в stdin через pipe; явная **UTF-8**, чтобы кириллица в комментариях/SQL не исказилась):
+
+```powershell
+Get-Content backup.sql -Encoding utf8 | docker exec -i remnawave-minishop-db sh -c 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"'
+```
+
+Если путь к файлу содержит пробелы, используйте кавычки: `Get-Content "E:\backups\backup.sql" -Encoding utf8 | ...`
+
4. Запустите приложение снова:
```bash
diff --git a/locales/en.json b/locales/en.json
index 2c8e08c..7c9ea8b 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -979,8 +979,33 @@
"admin_stats_trend_new_today": "Registrations today: {count}",
"admin_stats_section_revenue": "Revenue",
"admin_stats_section_revenue_hint": "Succeeded payments, shop currency",
- "admin_stats_revenue_chart_title": "Daily revenue (UTC, 14 days)",
- "admin_stats_revenue_chart_aria": "Daily revenue chart for the last 14 days",
+ "admin_stats_revenue_chart_title": "Daily revenue (UTC)",
+ "admin_stats_revenue_chart_aria": "Daily revenue chart; hover for date and amount",
+ "admin_stats_revenue_chart_aria_period": "Daily revenue, last {days} days (UTC)",
+ "admin_stats_revenue_chart_hint": "Hover the chart to see each day’s total",
+ "admin_stats_revenue_chart_range_sum": "Selected range total: {value}",
+ "admin_stats_revenue_chart_days_available": "Series length: {count} days",
+ "admin_stats_revenue_chart_uplot_time": "Time",
+ "admin_stats_revenue_chart_uplot_value": "Value",
+ "admin_stats_revenue_period_7": "7d",
+ "admin_stats_revenue_period_14": "14d",
+ "admin_stats_revenue_period_30": "30d",
+ "admin_stats_revenue_period_90": "90d",
+ "admin_stats_revenue_period_180": "6 mo",
+ "admin_stats_revenue_period_365": "1 yr",
+ "admin_stats_revenue_period_custom": "Custom",
+ "admin_stats_revenue_custom_range_title": "Date range (UTC)",
+ "admin_stats_revenue_custom_range_apply": "Apply",
+ "admin_stats_revenue_granularity_aria": "Revenue chart step",
+ "admin_stats_revenue_granularity_day": "By day",
+ "admin_stats_revenue_granularity_week": "By week",
+ "admin_stats_revenue_granularity_month": "By month",
+ "admin_stats_revenue_chart_hint_week": "Weeks start on Monday (UTC); hover the chart for totals",
+ "admin_stats_revenue_chart_hint_month": "Months are calendar months (UTC); hover the chart for totals",
+ "admin_stats_revenue_chart_bucket_count": "Points: {count}",
+ "admin_stats_revenue_chart_custom_span": "Range: {days} d.",
+ "admin_stats_revenue_tooltip_day": "Day",
+ "admin_stats_revenue_tooltip_amount": "Amount",
"admin_stats_revenue_avg_check": "Average ticket today: {value}",
"admin_stats_revenue_avg_none": "No successful payments today",
"admin_stats_revenue_avg_ticket_label": "Avg. ticket (today)",
@@ -1005,7 +1030,8 @@
"admin_stats_panel_disabled": "Disabled",
"admin_stats_panel_limited": "Limited",
"admin_stats_panel_total_users": "Total in panel",
- "admin_stats_panel_nodes_online": "Nodes online",
+ "admin_stats_panel_nodes_online": "Nodes in breakdown",
+ "admin_stats_panel_nodes_online_hint": "Same as the number of rows in the per-node section below (7-day window from bandwidth or node metrics). Not the same as system.nodes.totalOnline from /system/stats, which often counts backends/inbounds and can exceed visible nodes.",
"admin_stats_panel_memory": "Memory",
"admin_stats_panel_bw_week": "Traffic 7 days",
"admin_stats_panel_bw_month": "Traffic 30 days",
diff --git a/locales/ru.json b/locales/ru.json
index 8021600..666bee7 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -979,8 +979,33 @@
"admin_stats_trend_new_today": "Регистраций сегодня: {count}",
"admin_stats_section_revenue": "Доходы",
"admin_stats_section_revenue_hint": "Успешные платежи, валюта магазина",
- "admin_stats_revenue_chart_title": "Выручка по дням (UTC, 14 дней)",
- "admin_stats_revenue_chart_aria": "График выручки по дням за последние 14 дней",
+ "admin_stats_revenue_chart_title": "Выручка по дням (UTC)",
+ "admin_stats_revenue_chart_aria": "График дневной выручки; наведите курсор для даты и суммы",
+ "admin_stats_revenue_chart_aria_period": "Дневная выручка, последние {days} дн. (UTC)",
+ "admin_stats_revenue_chart_hint": "Наведите на график — дата и сумма за день",
+ "admin_stats_revenue_chart_range_sum": "Сумма за выбранный период: {value}",
+ "admin_stats_revenue_chart_days_available": "В выборке: {count} дн.",
+ "admin_stats_revenue_chart_uplot_time": "Время",
+ "admin_stats_revenue_chart_uplot_value": "Сумма",
+ "admin_stats_revenue_period_7": "7 дн.",
+ "admin_stats_revenue_period_14": "14 дн.",
+ "admin_stats_revenue_period_30": "30 дн.",
+ "admin_stats_revenue_period_90": "90 дн.",
+ "admin_stats_revenue_period_180": "6 мес.",
+ "admin_stats_revenue_period_365": "1 г.",
+ "admin_stats_revenue_period_custom": "Свой период",
+ "admin_stats_revenue_custom_range_title": "Диапазон дат (UTC)",
+ "admin_stats_revenue_custom_range_apply": "Применить",
+ "admin_stats_revenue_granularity_aria": "Шаг графика выручки",
+ "admin_stats_revenue_granularity_day": "По дням",
+ "admin_stats_revenue_granularity_week": "По неделям",
+ "admin_stats_revenue_granularity_month": "По месяцам",
+ "admin_stats_revenue_chart_hint_week": "Недели с понедельника (UTC); наведите на график для суммы",
+ "admin_stats_revenue_chart_hint_month": "Календарные месяцы (UTC); наведите на график для суммы",
+ "admin_stats_revenue_chart_bucket_count": "Точек: {count}",
+ "admin_stats_revenue_chart_custom_span": "Диапазон: {days} дн.",
+ "admin_stats_revenue_tooltip_day": "День",
+ "admin_stats_revenue_tooltip_amount": "Сумма",
"admin_stats_revenue_avg_check": "Средний чек сегодня: {value}",
"admin_stats_revenue_avg_none": "Сегодня без успешных платежей",
"admin_stats_revenue_avg_ticket_label": "Средний чек (сегодня)",
@@ -1005,7 +1030,8 @@
"admin_stats_panel_disabled": "Отключено",
"admin_stats_panel_limited": "Лимит",
"admin_stats_panel_total_users": "Всего в панели",
- "admin_stats_panel_nodes_online": "Нод онлайн",
+ "admin_stats_panel_nodes_online": "Нод в списке",
+ "admin_stats_panel_nodes_online_hint": "Совпадает с числом строк в блоке «По нодам (7 дней)» ниже (данные bandwidth или метрик нод). Это не поле system.nodes.totalOnline из /system/stats — там часто считаются бэкенды/inbounds и число может быть больше, чем уникальных нод.",
"admin_stats_panel_memory": "Память",
"admin_stats_panel_bw_week": "Трафик 7 дней",
"admin_stats_panel_bw_month": "Трафик 30 дней",
diff --git a/package-lock.json b/package-lock.json
index 8348b80..e0a64d5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,6 +6,7 @@
"": {
"devDependencies": {
"@eslint/js": "^9.39.2",
+ "@internationalized/date": "^3.12.1",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"@tailwindcss/cli": "4.2.4",
"@tailwindcss/vite": "^4.3.0",
@@ -24,6 +25,7 @@
"svelte-eslint-parser": "^1.4.1",
"tailwind-merge": "^3.5.0",
"tailwindcss": "4.2.4",
+ "uplot": "^1.6.32",
"vite": "^8.0.10"
}
},
@@ -760,7 +762,6 @@
"integrity": "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@swc/helpers": "^0.5.0"
}
@@ -1489,7 +1490,6 @@
"integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
@@ -4014,6 +4014,13 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/uplot": {
+ "version": "1.6.32",
+ "resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.32.tgz",
+ "integrity": "sha512-KIMVnG68zvu5XXUbC4LQEPnhwOxBuLyW1AHtpm6IKTXImkbLgkMy+jabjLgSLMasNuGGzQm/ep3tOkyTxpiQIw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
diff --git a/package.json b/package.json
index 65da2bc..c3f43c3 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,8 @@
"fix": "npm run format && npm run lint:fix"
},
"devDependencies": {
+ "@eslint/js": "^9.39.2",
+ "@internationalized/date": "^3.12.1",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"@tailwindcss/cli": "4.2.4",
"@tailwindcss/vite": "^4.3.0",
@@ -28,18 +30,18 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"esbuild": "^0.28.0",
- "lucide-svelte": "^1.0.1",
- "svelte": "^5.55.5",
- "tailwind-merge": "^3.5.0",
- "tailwindcss": "4.2.4",
- "vite": "^8.0.10",
- "@eslint/js": "^9.39.2",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.14.0",
"globals": "^16.5.0",
+ "lucide-svelte": "^1.0.1",
"prettier": "^3.7.4",
"prettier-plugin-svelte": "^3.4.0",
- "svelte-eslint-parser": "^1.4.1"
+ "svelte": "^5.55.5",
+ "svelte-eslint-parser": "^1.4.1",
+ "tailwind-merge": "^3.5.0",
+ "tailwindcss": "4.2.4",
+ "uplot": "^1.6.32",
+ "vite": "^8.0.10"
}
}