feat(dashboard): add long-range windows and refresh controls
This commit is contained in:
@@ -61,11 +61,11 @@ std::string page(Request &, Response &response) {
|
|||||||
--map-data-min: #93c5fd;
|
--map-data-min: #93c5fd;
|
||||||
--map-data-mid: #2563eb;
|
--map-data-mid: #2563eb;
|
||||||
--map-data-max: #1e3a8a;
|
--map-data-max: #1e3a8a;
|
||||||
--chart-requests: linear-gradient(180deg, #0284c7 0%, #0891b2 52%, #65a30d 100%);
|
--chart-requests: linear-gradient(180deg, #93c5fd 0%, #2563eb 58%, #1e3a8a 100%);
|
||||||
--chart-rules: linear-gradient(180deg, #7c3aed 0%, #2563eb 48%, #0891b2 100%);
|
--chart-rules: linear-gradient(180deg, #93c5fd 0%, #2563eb 58%, #1e3a8a 100%);
|
||||||
--rank-track: rgba(15, 23, 42, 0.08);
|
--rank-track: rgba(15, 23, 42, 0.08);
|
||||||
--rank-request: linear-gradient(90deg, #0ea5e9 0%, #65a30d 100%);
|
--rank-request: linear-gradient(90deg, #93c5fd 0%, #2563eb 62%, #1e3a8a 100%);
|
||||||
--rank-rule: linear-gradient(90deg, #7c3aed 0%, #0891b2 100%);
|
--rank-rule: linear-gradient(90deg, #93c5fd 0%, #2563eb 62%, #1e3a8a 100%);
|
||||||
--danger: #dc2626;
|
--danger: #dc2626;
|
||||||
--warn: #b45309;
|
--warn: #b45309;
|
||||||
}
|
}
|
||||||
@@ -93,11 +93,11 @@ std::string page(Request &, Response &response) {
|
|||||||
--map-data-min: #7dd3fc;
|
--map-data-min: #7dd3fc;
|
||||||
--map-data-mid: #38bdf8;
|
--map-data-mid: #38bdf8;
|
||||||
--map-data-max: #2563eb;
|
--map-data-max: #2563eb;
|
||||||
--chart-requests: linear-gradient(180deg, #38bdf8 0%, #22d3ee 45%, #84cc16 100%);
|
--chart-requests: linear-gradient(180deg, #7dd3fc 0%, #38bdf8 56%, #2563eb 100%);
|
||||||
--chart-rules: linear-gradient(180deg, #a78bfa 0%, #60a5fa 48%, #22d3ee 100%);
|
--chart-rules: linear-gradient(180deg, #7dd3fc 0%, #38bdf8 56%, #2563eb 100%);
|
||||||
--rank-track: rgba(148, 163, 184, 0.16);
|
--rank-track: rgba(148, 163, 184, 0.16);
|
||||||
--rank-request: linear-gradient(90deg, #22d3ee 0%, #84cc16 100%);
|
--rank-request: linear-gradient(90deg, #7dd3fc 0%, #38bdf8 58%, #2563eb 100%);
|
||||||
--rank-rule: linear-gradient(90deg, #a78bfa 0%, #22d3ee 100%);
|
--rank-rule: linear-gradient(90deg, #7dd3fc 0%, #38bdf8 58%, #2563eb 100%);
|
||||||
--danger: #f87171;
|
--danger: #f87171;
|
||||||
--warn: #fbbf24;
|
--warn: #fbbf24;
|
||||||
}
|
}
|
||||||
@@ -277,6 +277,7 @@ std::string page(Request &, Response &response) {
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
.metric-grid.two-up { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
.metric-grid.two-up { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
.conversion-totals { margin-bottom: 12px; }
|
||||||
.window-grid {
|
.window-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(280px, 0.9fr) minmax(0, 1.1fr);
|
grid-template-columns: minmax(280px, 0.9fr) minmax(0, 1.1fr);
|
||||||
@@ -684,7 +685,7 @@ std::string page(Request &, Response &response) {
|
|||||||
<span data-lang="en">Refresh</span><span data-lang="zh">刷新</span>
|
<span data-lang="en">Refresh</span><span data-lang="zh">刷新</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="menu-wrap">
|
<div class="menu-wrap">
|
||||||
<button type="button" id="refresh-interval-button" aria-haspopup="menu" aria-expanded="false">Auto: 30s</button>
|
<button type="button" id="refresh-interval-button" aria-haspopup="menu" aria-expanded="false">Auto: 3s</button>
|
||||||
<div class="refresh-menu" id="refresh-menu" role="menu" hidden></div>
|
<div class="refresh-menu" id="refresh-menu" role="menu" hidden></div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" id="lang-toggle">EN</button>
|
<button type="button" id="lang-toggle">EN</button>
|
||||||
@@ -804,18 +805,34 @@ std::string page(Request &, Response &response) {
|
|||||||
{ key: "day", en: "1 Day", zh: "1 天" },
|
{ key: "day", en: "1 Day", zh: "1 天" },
|
||||||
{ key: "seven_days", en: "7 Days", zh: "7 天" },
|
{ key: "seven_days", en: "7 Days", zh: "7 天" },
|
||||||
{ key: "thirty_days", en: "30 Days", zh: "30 天" },
|
{ key: "thirty_days", en: "30 Days", zh: "30 天" },
|
||||||
|
{ key: "half_year", en: "Half Year", zh: "半年" },
|
||||||
|
{ key: "year", en: "1 Year", zh: "1 年" },
|
||||||
{ key: "lifetime", en: "Lifetime", zh: "历史总计" }
|
{ key: "lifetime", en: "Lifetime", zh: "历史总计" }
|
||||||
];
|
];
|
||||||
|
var SUMMARY_WINDOWS = [
|
||||||
|
{ key: "hour", en: "1 Hour", zh: "1 小时" },
|
||||||
|
{ key: "day", en: "1 Day", zh: "1 天" },
|
||||||
|
{ key: "seven_days", en: "7 Days", zh: "7 天" },
|
||||||
|
{ key: "thirty_days", en: "30 Days", zh: "30 天" },
|
||||||
|
{ key: "half_year", en: "Half Year", zh: "半年" },
|
||||||
|
{ key: "year", en: "1 Year", zh: "1 年" }
|
||||||
|
];
|
||||||
var REFRESH_OPTIONS = [
|
var REFRESH_OPTIONS = [
|
||||||
{ seconds: 0, en: "Pause", zh: "暂停" },
|
{ seconds: 0, en: "Pause", zh: "暂停" },
|
||||||
|
{ seconds: 1, en: "1s", zh: "1 秒" },
|
||||||
|
{ seconds: 3, en: "3s", zh: "3 秒" },
|
||||||
|
{ seconds: 5, en: "5s", zh: "5 秒" },
|
||||||
{ seconds: 10, en: "10s", zh: "10 秒" },
|
{ seconds: 10, en: "10s", zh: "10 秒" },
|
||||||
{ seconds: 30, en: "30s", zh: "30 秒" },
|
{ seconds: 30, en: "30s", zh: "30 秒" },
|
||||||
{ seconds: 60, en: "1m", zh: "1 分钟" },
|
{ seconds: 60, en: "1m", zh: "1 分钟" },
|
||||||
{ seconds: 300, en: "5m", zh: "5 分钟" }
|
{ seconds: 300, en: "5m", zh: "5 分钟" }
|
||||||
];
|
];
|
||||||
var selectedMapWindow = localStorage.getItem("sce-dashboard-map-window") || "lifetime";
|
var MAP_WINDOW_STORAGE_KEY = "sce-dashboard-map-window-v2";
|
||||||
var selectedRankingWindow = localStorage.getItem("sce-dashboard-ranking-window") || "lifetime";
|
var RANKING_WINDOW_STORAGE_KEY = "sce-dashboard-ranking-window-v2";
|
||||||
var refreshIntervalSeconds = Number(localStorage.getItem("sce-dashboard-refresh-interval") || 30);
|
var REFRESH_INTERVAL_STORAGE_KEY = "sce-dashboard-refresh-interval-v2";
|
||||||
|
var selectedMapWindow = localStorage.getItem(MAP_WINDOW_STORAGE_KEY) || "lifetime";
|
||||||
|
var selectedRankingWindow = localStorage.getItem(RANKING_WINDOW_STORAGE_KEY) || "lifetime";
|
||||||
|
var refreshIntervalSeconds = Number(localStorage.getItem(REFRESH_INTERVAL_STORAGE_KEY) || 3);
|
||||||
var refreshTimer = null;
|
var refreshTimer = null;
|
||||||
|
|
||||||
function isZh() { return /^zh\b/i.test(document.documentElement.lang); }
|
function isZh() { return /^zh\b/i.test(document.documentElement.lang); }
|
||||||
@@ -948,15 +965,13 @@ std::string page(Request &, Response &response) {
|
|||||||
runtimeCardHtml("Total Runtime", "累计运行时长", duration(runtime.total_runtime_seconds), "Persisted across restarts.", "跨重启持久化累计。") +
|
runtimeCardHtml("Total Runtime", "累计运行时长", duration(runtime.total_runtime_seconds), "Persisted across restarts.", "跨重启持久化累计。") +
|
||||||
runtimeCardHtml("Launches", "启动次数", number(runtime.launch_count), "Number of launches recorded in the statistics file.", "统计文件记录到的启动次数。") +
|
runtimeCardHtml("Launches", "启动次数", number(runtime.launch_count), "Number of launches recorded in the statistics file.", "统计文件记录到的启动次数。") +
|
||||||
'</div></section>' +
|
'</div></section>' +
|
||||||
'<section class="stat-block"><div class="block-head"><div><h2>' + text("Conversion Totals", "转换总览") + '</h2>' +
|
'<section class="stat-block"><div class="block-head"><div><h2>' + text("Conversion Overview", "转换总览") + '</h2>' +
|
||||||
'<div class="block-copy">' + text("Requests and rule conversions by scope", "按统计范围查看请求与规则转换") + '</div></div></div>' +
|
'<div class="block-copy">' + text("Current-process and persisted totals with rolling comparison windows", "本次启动、历史总计与滚动时间范围对比") + '</div></div></div>' +
|
||||||
'<div class="metric-grid two-up">' +
|
'<div class="metric-grid two-up conversion-totals">' +
|
||||||
countersPairHtml("Since Start", "本次启动", windows.startup, "Only conversions after the current process started.", "仅统计当前进程启动后的转换。") +
|
countersPairHtml("Since Start", "本次启动", windows.startup, "Only conversions after the current process started.", "仅统计当前进程启动后的转换。") +
|
||||||
countersPairHtml("Lifetime", "历史总计", windows.lifetime, "Persisted conversion totals since the statistics file was created.", "统计文件创建以来持久化累计的转换。") +
|
countersPairHtml("Lifetime", "历史总计", windows.lifetime, "Persisted conversion totals since the statistics file was created.", "统计文件创建以来持久化累计的转换。") +
|
||||||
'</div></section>' +
|
'</div>' +
|
||||||
'<section class="stat-block"><div class="block-head"><div><h2>' + text("Time Ranges", "统计时间范围") + '</h2>' +
|
'<div class="window-strip">' + SUMMARY_WINDOWS.map(function (item) { return miniWindowHtml(item, windows[item.key]); }).join("") + '</div></section>';
|
||||||
'<div class="block-copy">' + text("Requests and rule conversions across common time ranges", "按常用时间范围查看请求与规则转换") + '</div></div></div>' +
|
|
||||||
'<div class="window-strip">' + RANGE_WINDOWS.map(function (item) { return miniWindowHtml(item, windows[item.key]); }).join("") + '</div></section>';
|
|
||||||
animateCounters(metricsEl);
|
animateCounters(metricsEl);
|
||||||
}
|
}
|
||||||
function renderHourlyChart(container, series, field, className, labelEn, labelZh) {
|
function renderHourlyChart(container, series, field, className, labelEn, labelZh) {
|
||||||
@@ -1002,7 +1017,7 @@ std::string page(Request &, Response &response) {
|
|||||||
mapTabs.querySelectorAll("[data-map-window]").forEach(function (button) {
|
mapTabs.querySelectorAll("[data-map-window]").forEach(function (button) {
|
||||||
button.addEventListener("click", function () {
|
button.addEventListener("click", function () {
|
||||||
selectedMapWindow = button.getAttribute("data-map-window");
|
selectedMapWindow = button.getAttribute("data-map-window");
|
||||||
localStorage.setItem("sce-dashboard-map-window", selectedMapWindow);
|
localStorage.setItem(MAP_WINDOW_STORAGE_KEY, selectedMapWindow);
|
||||||
updateRangeTabs(mapTabs, "data-map-window", selectedMapWindow);
|
updateRangeTabs(mapTabs, "data-map-window", selectedMapWindow);
|
||||||
if (latest) {
|
if (latest) {
|
||||||
renderMapCountries(countriesForWindow(latest, selectedMapWindow));
|
renderMapCountries(countriesForWindow(latest, selectedMapWindow));
|
||||||
@@ -1014,7 +1029,7 @@ std::string page(Request &, Response &response) {
|
|||||||
rankingTabs.querySelectorAll("[data-ranking-window]").forEach(function (button) {
|
rankingTabs.querySelectorAll("[data-ranking-window]").forEach(function (button) {
|
||||||
button.addEventListener("click", function () {
|
button.addEventListener("click", function () {
|
||||||
selectedRankingWindow = button.getAttribute("data-ranking-window");
|
selectedRankingWindow = button.getAttribute("data-ranking-window");
|
||||||
localStorage.setItem("sce-dashboard-ranking-window", selectedRankingWindow);
|
localStorage.setItem(RANKING_WINDOW_STORAGE_KEY, selectedRankingWindow);
|
||||||
updateRangeTabs(rankingTabs, "data-ranking-window", selectedRankingWindow);
|
updateRangeTabs(rankingTabs, "data-ranking-window", selectedRankingWindow);
|
||||||
if (latest)
|
if (latest)
|
||||||
renderRankingCountries(countriesForWindow(latest, selectedRankingWindow));
|
renderRankingCountries(countriesForWindow(latest, selectedRankingWindow));
|
||||||
@@ -1045,7 +1060,7 @@ std::string page(Request &, Response &response) {
|
|||||||
refreshMenu.querySelectorAll("[data-refresh-seconds]").forEach(function (button) {
|
refreshMenu.querySelectorAll("[data-refresh-seconds]").forEach(function (button) {
|
||||||
button.addEventListener("click", function () {
|
button.addEventListener("click", function () {
|
||||||
refreshIntervalSeconds = Number(button.getAttribute("data-refresh-seconds")) || 0;
|
refreshIntervalSeconds = Number(button.getAttribute("data-refresh-seconds")) || 0;
|
||||||
localStorage.setItem("sce-dashboard-refresh-interval", String(refreshIntervalSeconds));
|
localStorage.setItem(REFRESH_INTERVAL_STORAGE_KEY, String(refreshIntervalSeconds));
|
||||||
refreshMenu.hidden = true;
|
refreshMenu.hidden = true;
|
||||||
refreshIntervalButton.setAttribute("aria-expanded", "false");
|
refreshIntervalButton.setAttribute("aria-expanded", "false");
|
||||||
updateRefreshIntervalUi();
|
updateRefreshIntervalUi();
|
||||||
@@ -1138,9 +1153,9 @@ std::string page(Request &, Response &response) {
|
|||||||
var height = node.clientHeight || 430;
|
var height = node.clientHeight || 430;
|
||||||
var styles = getComputedStyle(document.documentElement);
|
var styles = getComputedStyle(document.documentElement);
|
||||||
var emptyColor = styles.getPropertyValue("--map-empty").trim() || "#cbd5e1";
|
var emptyColor = styles.getPropertyValue("--map-empty").trim() || "#cbd5e1";
|
||||||
var dataMinColor = styles.getPropertyValue("--map-data-min").trim() || "#22c7d9";
|
var dataMinColor = styles.getPropertyValue("--map-data-min").trim() || "#93c5fd";
|
||||||
var dataMidColor = styles.getPropertyValue("--map-data-mid").trim() || "#0ea5e9";
|
var dataMidColor = styles.getPropertyValue("--map-data-mid").trim() || "#2563eb";
|
||||||
var dataMaxColor = styles.getPropertyValue("--map-data-max").trim() || "#15803d";
|
var dataMaxColor = styles.getPropertyValue("--map-data-max").trim() || "#1e3a8a";
|
||||||
svg.attr("viewBox", "0 0 " + width + " " + height);
|
svg.attr("viewBox", "0 0 " + width + " " + height);
|
||||||
svg.selectAll("*").remove();
|
svg.selectAll("*").remove();
|
||||||
var projection = d3.geoNaturalEarth1().rotate([-150, 0]).fitSize([width, height], { type: "Sphere" });
|
var projection = d3.geoNaturalEarth1().rotate([-150, 0]).fitSize([width, height], { type: "Sphere" });
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ namespace {
|
|||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
constexpr size_t kBucketCount = 30 * 24 * 60;
|
constexpr size_t kBucketCount = 30 * 24 * 60;
|
||||||
|
constexpr size_t kDailyBucketCount = 366;
|
||||||
|
|
||||||
struct Counters {
|
struct Counters {
|
||||||
uint64_t subscription_requests = 0;
|
uint64_t subscription_requests = 0;
|
||||||
@@ -50,6 +51,12 @@ struct Bucket {
|
|||||||
std::vector<CountryBucketEntry> countries;
|
std::vector<CountryBucketEntry> countries;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct DailyBucket {
|
||||||
|
int64_t day = 0;
|
||||||
|
Counters counters;
|
||||||
|
std::vector<CountryBucketEntry> countries;
|
||||||
|
};
|
||||||
|
|
||||||
struct SnapshotCountry {
|
struct SnapshotCountry {
|
||||||
std::string code;
|
std::string code;
|
||||||
CountryCounters counters;
|
CountryCounters counters;
|
||||||
@@ -70,6 +77,7 @@ struct State {
|
|||||||
std::map<std::string, CountryCounters> startup_countries;
|
std::map<std::string, CountryCounters> startup_countries;
|
||||||
std::map<std::string, CountryCounters> lifetime_countries;
|
std::map<std::string, CountryCounters> lifetime_countries;
|
||||||
std::array<Bucket, kBucketCount> buckets;
|
std::array<Bucket, kBucketCount> buckets;
|
||||||
|
std::array<DailyBucket, kDailyBucketCount> daily_buckets;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::mutex g_mutex;
|
std::mutex g_mutex;
|
||||||
@@ -246,6 +254,43 @@ std::vector<SnapshotCountry> countryWindowLocked(int64_t now_minute,
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Counters dailyWindowCountersLocked(int64_t now_day, int days) {
|
||||||
|
Counters result;
|
||||||
|
if (!g_state)
|
||||||
|
return result;
|
||||||
|
int64_t earliest = now_day - days + 1;
|
||||||
|
for (const DailyBucket &bucket : g_state->daily_buckets) {
|
||||||
|
if (bucket.day >= earliest && bucket.day <= now_day) {
|
||||||
|
addCounters(result, bucket.counters.subscription_requests,
|
||||||
|
bucket.counters.rule_conversions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<SnapshotCountry> countryDailyWindowLocked(int64_t now_day,
|
||||||
|
int days) {
|
||||||
|
std::map<std::string, CountryCounters> totals;
|
||||||
|
if (!g_state)
|
||||||
|
return {};
|
||||||
|
int64_t earliest = now_day - days + 1;
|
||||||
|
for (const DailyBucket &bucket : g_state->daily_buckets) {
|
||||||
|
if (bucket.day < earliest || bucket.day > now_day)
|
||||||
|
continue;
|
||||||
|
for (const CountryBucketEntry &entry : bucket.countries) {
|
||||||
|
addCountryCounters(totals, entry.code,
|
||||||
|
entry.counters.subscription_requests,
|
||||||
|
entry.counters.rule_conversions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<SnapshotCountry> result;
|
||||||
|
result.reserve(totals.size());
|
||||||
|
for (const auto &entry : totals)
|
||||||
|
result.push_back({entry.first, entry.second});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<SnapshotCountry>
|
std::vector<SnapshotCountry>
|
||||||
countrySnapshotLocked(const std::map<std::string, CountryCounters> &source) {
|
countrySnapshotLocked(const std::map<std::string, CountryCounters> &source) {
|
||||||
std::vector<SnapshotCountry> result;
|
std::vector<SnapshotCountry> result;
|
||||||
@@ -314,6 +359,33 @@ json countriesObjectJson(const std::map<std::string, CountryCounters> &source) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void seedDailyBucketsFromMinuteBucketsLocked() {
|
||||||
|
if (!g_state)
|
||||||
|
return;
|
||||||
|
for (const Bucket &bucket : g_state->buckets) {
|
||||||
|
if (bucket.minute <= 0)
|
||||||
|
continue;
|
||||||
|
if (!bucket.counters.subscription_requests &&
|
||||||
|
!bucket.counters.rule_conversions)
|
||||||
|
continue;
|
||||||
|
int64_t day = bucket.minute / (24 * 60);
|
||||||
|
size_t index = static_cast<size_t>(day % kDailyBucketCount);
|
||||||
|
if (g_state->daily_buckets[index].day != day) {
|
||||||
|
g_state->daily_buckets[index].day = day;
|
||||||
|
g_state->daily_buckets[index].counters = Counters();
|
||||||
|
g_state->daily_buckets[index].countries.clear();
|
||||||
|
}
|
||||||
|
addCounters(g_state->daily_buckets[index].counters,
|
||||||
|
bucket.counters.subscription_requests,
|
||||||
|
bucket.counters.rule_conversions);
|
||||||
|
for (const CountryBucketEntry &entry : bucket.countries) {
|
||||||
|
addCountryCounters(g_state->daily_buckets[index].countries, entry.code,
|
||||||
|
entry.counters.subscription_requests,
|
||||||
|
entry.counters.rule_conversions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int64_t currentUptimeLocked(int64_t now) {
|
int64_t currentUptimeLocked(int64_t now) {
|
||||||
if (!g_state || g_state->started_at <= 0 || now <= g_state->started_at)
|
if (!g_state || g_state->started_at <= 0 || now <= g_state->started_at)
|
||||||
return 0;
|
return 0;
|
||||||
@@ -334,7 +406,7 @@ void loadLocked() {
|
|||||||
try {
|
try {
|
||||||
json root = json::parse(content);
|
json root = json::parse(content);
|
||||||
int schema = root.value("schema", 1);
|
int schema = root.value("schema", 1);
|
||||||
if (schema < 1 || schema > 2)
|
if (schema < 1 || schema > 3)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
g_state->last_flush = root.value("updated_at", 0LL);
|
g_state->last_flush = root.value("updated_at", 0LL);
|
||||||
@@ -389,6 +461,38 @@ void loadLocked() {
|
|||||||
rules);
|
rules);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool loaded_daily_buckets = false;
|
||||||
|
auto daily_buckets = root.value("daily_buckets", json::array());
|
||||||
|
for (const auto &item : daily_buckets) {
|
||||||
|
int64_t day = item.value("day", 0LL);
|
||||||
|
if (day <= 0)
|
||||||
|
continue;
|
||||||
|
size_t index = static_cast<size_t>(day % kDailyBucketCount);
|
||||||
|
g_state->daily_buckets[index].day = day;
|
||||||
|
g_state->daily_buckets[index].counters.subscription_requests =
|
||||||
|
item.value("subscription_requests", 0ULL);
|
||||||
|
g_state->daily_buckets[index].counters.rule_conversions =
|
||||||
|
item.value("rule_conversions", 0ULL);
|
||||||
|
g_state->daily_buckets[index].countries.clear();
|
||||||
|
|
||||||
|
auto country_items = item.value("countries", json::array());
|
||||||
|
for (const auto &country_item : country_items) {
|
||||||
|
std::string code =
|
||||||
|
normalizeCountryCode(country_item.value("code", "ZZ"));
|
||||||
|
uint64_t requests =
|
||||||
|
country_item.value("subscription_requests", 0ULL);
|
||||||
|
uint64_t rules = country_item.value("rule_conversions", 0ULL);
|
||||||
|
if (requests || rules)
|
||||||
|
addCountryCounters(g_state->daily_buckets[index].countries, code,
|
||||||
|
requests, rules);
|
||||||
|
}
|
||||||
|
if (g_state->daily_buckets[index].counters.subscription_requests ||
|
||||||
|
g_state->daily_buckets[index].counters.rule_conversions)
|
||||||
|
loaded_daily_buckets = true;
|
||||||
|
}
|
||||||
|
if (!loaded_daily_buckets)
|
||||||
|
seedDailyBucketsFromMinuteBucketsLocked();
|
||||||
} catch (const std::exception &e) {
|
} catch (const std::exception &e) {
|
||||||
writeLog(0, "统计数据加载失败:" + std::string(e.what()), LOG_LEVEL_WARNING);
|
writeLog(0, "统计数据加载失败:" + std::string(e.what()), LOG_LEVEL_WARNING);
|
||||||
}
|
}
|
||||||
@@ -409,7 +513,7 @@ bool flushLocked(bool stopping, int64_t now) {
|
|||||||
g_state->last_stopped_at = now;
|
g_state->last_stopped_at = now;
|
||||||
|
|
||||||
json root;
|
json root;
|
||||||
root["schema"] = 2;
|
root["schema"] = 3;
|
||||||
root["updated_at"] = now;
|
root["updated_at"] = now;
|
||||||
root["runtime"] = {
|
root["runtime"] = {
|
||||||
{"first_started_at", g_state->first_started_at},
|
{"first_started_at", g_state->first_started_at},
|
||||||
@@ -449,6 +553,33 @@ bool flushLocked(bool stopping, int64_t now) {
|
|||||||
}
|
}
|
||||||
root["buckets"] = buckets;
|
root["buckets"] = buckets;
|
||||||
|
|
||||||
|
json daily_buckets = json::array();
|
||||||
|
for (const DailyBucket &bucket : g_state->daily_buckets) {
|
||||||
|
if (bucket.day <= 0)
|
||||||
|
continue;
|
||||||
|
if (!bucket.counters.subscription_requests &&
|
||||||
|
!bucket.counters.rule_conversions)
|
||||||
|
continue;
|
||||||
|
json countries = json::array();
|
||||||
|
for (const CountryBucketEntry &entry : bucket.countries) {
|
||||||
|
if (!entry.counters.subscription_requests &&
|
||||||
|
!entry.counters.rule_conversions)
|
||||||
|
continue;
|
||||||
|
countries.push_back({{"code", entry.code},
|
||||||
|
{"subscription_requests",
|
||||||
|
entry.counters.subscription_requests},
|
||||||
|
{"rule_conversions",
|
||||||
|
entry.counters.rule_conversions}});
|
||||||
|
}
|
||||||
|
daily_buckets.push_back({{"day", bucket.day},
|
||||||
|
{"subscription_requests",
|
||||||
|
bucket.counters.subscription_requests},
|
||||||
|
{"rule_conversions",
|
||||||
|
bucket.counters.rule_conversions},
|
||||||
|
{"countries", countries}});
|
||||||
|
}
|
||||||
|
root["daily_buckets"] = daily_buckets;
|
||||||
|
|
||||||
std::string tmp = path + ".tmp";
|
std::string tmp = path + ".tmp";
|
||||||
if (!writeTextFile(tmp, root.dump()))
|
if (!writeTextFile(tmp, root.dump()))
|
||||||
return false;
|
return false;
|
||||||
@@ -530,6 +661,7 @@ void recordSubscriptionConversion(const Request &request,
|
|||||||
|
|
||||||
int64_t now = nowSeconds();
|
int64_t now = nowSeconds();
|
||||||
int64_t minute = now / 60;
|
int64_t minute = now / 60;
|
||||||
|
int64_t day = now / (24 * 60 * 60);
|
||||||
std::string country = countryFromHeaders(request);
|
std::string country = countryFromHeaders(request);
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(g_mutex);
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
@@ -552,6 +684,17 @@ void recordSubscriptionConversion(const Request &request,
|
|||||||
addCountryCounters(g_state->buckets[index].countries, country, 1,
|
addCountryCounters(g_state->buckets[index].countries, country, 1,
|
||||||
rule_conversions);
|
rule_conversions);
|
||||||
|
|
||||||
|
size_t daily_index = static_cast<size_t>(day % kDailyBucketCount);
|
||||||
|
if (g_state->daily_buckets[daily_index].day != day) {
|
||||||
|
g_state->daily_buckets[daily_index].day = day;
|
||||||
|
g_state->daily_buckets[daily_index].counters = Counters();
|
||||||
|
g_state->daily_buckets[daily_index].countries.clear();
|
||||||
|
}
|
||||||
|
addCounters(g_state->daily_buckets[daily_index].counters, 1,
|
||||||
|
rule_conversions);
|
||||||
|
addCountryCounters(g_state->daily_buckets[daily_index].countries, country, 1,
|
||||||
|
rule_conversions);
|
||||||
|
|
||||||
g_state->dirty = true;
|
g_state->dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,6 +713,7 @@ std::string dashboardData(RESPONSE_CALLBACK_ARGS) {
|
|||||||
std::lock_guard<std::mutex> lock(g_mutex);
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
int64_t now = nowSeconds();
|
int64_t now = nowSeconds();
|
||||||
int64_t now_minute = now / 60;
|
int64_t now_minute = now / 60;
|
||||||
|
int64_t now_day = now / (24 * 60 * 60);
|
||||||
|
|
||||||
json root;
|
json root;
|
||||||
root["enabled"] = global.statisticsEnabled;
|
root["enabled"] = global.statisticsEnabled;
|
||||||
@@ -590,6 +734,8 @@ std::string dashboardData(RESPONSE_CALLBACK_ARGS) {
|
|||||||
{"seven_days", countersJson(windowCountersLocked(now_minute, 7 * 24 * 60))},
|
{"seven_days", countersJson(windowCountersLocked(now_minute, 7 * 24 * 60))},
|
||||||
{"thirty_days",
|
{"thirty_days",
|
||||||
countersJson(windowCountersLocked(now_minute, 30 * 24 * 60))},
|
countersJson(windowCountersLocked(now_minute, 30 * 24 * 60))},
|
||||||
|
{"half_year", countersJson(dailyWindowCountersLocked(now_day, 183))},
|
||||||
|
{"year", countersJson(dailyWindowCountersLocked(now_day, 365))},
|
||||||
{"lifetime", countersJson(g_state ? g_state->lifetime : Counters())}};
|
{"lifetime", countersJson(g_state ? g_state->lifetime : Counters())}};
|
||||||
|
|
||||||
json country_windows = json::object();
|
json country_windows = json::object();
|
||||||
@@ -603,6 +749,9 @@ std::string dashboardData(RESPONSE_CALLBACK_ARGS) {
|
|||||||
countriesJson(countryWindowLocked(now_minute, 7 * 24 * 60));
|
countriesJson(countryWindowLocked(now_minute, 7 * 24 * 60));
|
||||||
country_windows["thirty_days"] =
|
country_windows["thirty_days"] =
|
||||||
countriesJson(countryWindowLocked(now_minute, 30 * 24 * 60));
|
countriesJson(countryWindowLocked(now_minute, 30 * 24 * 60));
|
||||||
|
country_windows["half_year"] =
|
||||||
|
countriesJson(countryDailyWindowLocked(now_day, 183));
|
||||||
|
country_windows["year"] = countriesJson(countryDailyWindowLocked(now_day, 365));
|
||||||
country_windows["lifetime"] =
|
country_windows["lifetime"] =
|
||||||
countriesJson(g_state ? countrySnapshotLocked(g_state->lifetime_countries)
|
countriesJson(g_state ? countrySnapshotLocked(g_state->lifetime_countries)
|
||||||
: std::vector<SnapshotCountry>());
|
: std::vector<SnapshotCountry>());
|
||||||
|
|||||||
Reference in New Issue
Block a user