sdfsf
This commit is contained in:
@@ -1045,7 +1045,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await request(`${apiBase}/projects/${encodeURIComponent(key)}/bootstrap`, { timeoutMs: 10000 });
|
||||
const params = new URLSearchParams({ view: activeTab || 'overview' });
|
||||
const data = await request(`${apiBase}/projects/${encodeURIComponent(key)}/bootstrap?${params.toString()}`, { timeoutMs: 10000 });
|
||||
const normalized = normalizeBootstrap(data, key);
|
||||
setPayload(normalized);
|
||||
setSettingsForm({
|
||||
@@ -2109,9 +2110,9 @@
|
||||
}),
|
||||
]),
|
||||
h('div', { key: 'charts', className: 'mc-overview-grid' }, [
|
||||
panel('Mining-Verlauf', 'Coins total ueber die Zeit.', h(SimpleChart, { type: 'line', data: overviewCharts.mining })),
|
||||
panel('Performance-Verlauf', 'Letzte DOGE-pro-Tag-Raten je Intervall.', h(SimpleChart, { type: 'area', data: overviewCharts.performance })),
|
||||
panel('Kurs-Verlauf', 'Historische Preiswerte der Messreihe.', h(SimpleChart, { type: 'line', data: overviewCharts.pricing })),
|
||||
panel('Mining-Verlauf', 'Coins total der letzten 15 Tage.', h(SimpleChart, { type: 'line', data: overviewCharts.mining })),
|
||||
panel('Performance-Verlauf', 'DOGE-pro-Tag-Raten der letzten 15 Tage.', h(SimpleChart, { type: 'area', data: overviewCharts.performance })),
|
||||
panel('Kurs-Verlauf', 'Preiswerte der letzten 15 Tage.', h(SimpleChart, { type: 'line', data: overviewCharts.pricing })),
|
||||
]),
|
||||
panel('Zielmonitor', 'Rest-DOGE und Resttage werden gegen den letzten verfuegbaren Kurs je Zielwaehrung berechnet.',
|
||||
h('div', { className: 'mc-target-grid' },
|
||||
|
||||
@@ -22,6 +22,7 @@ final class Router
|
||||
private const BOOTSTRAP_MEASUREMENT_LIMIT = 150;
|
||||
private const BOOTSTRAP_SNAPSHOT_LIMIT = 40;
|
||||
private const LONG_REQUEST_BUDGET_SECONDS = 8.0;
|
||||
private const OVERVIEW_WINDOW_DAYS = 15;
|
||||
|
||||
private string $moduleBasePath;
|
||||
private ModuleConfig $config;
|
||||
@@ -164,7 +165,8 @@ final class Router
|
||||
}
|
||||
|
||||
if ($resource === 'bootstrap' && $method === 'GET') {
|
||||
$this->respond(['data' => $this->bootstrap($projectKey)]);
|
||||
$view = trim((string) ($_GET['view'] ?? 'overview'));
|
||||
$this->respond(['data' => $this->bootstrap($projectKey, $view)]);
|
||||
}
|
||||
|
||||
if ($resource === 'measurements' && $method === 'GET') {
|
||||
@@ -310,11 +312,13 @@ final class Router
|
||||
}
|
||||
}
|
||||
|
||||
private function bootstrap(string $projectKey): array
|
||||
private function bootstrap(string $projectKey, string $view = 'overview'): array
|
||||
{
|
||||
$startedAt = microtime(true);
|
||||
$view = $this->normalizeBootstrapView($view);
|
||||
$this->debug->add('bootstrap.start', [
|
||||
'project_key' => $projectKey,
|
||||
'view' => $view,
|
||||
'measurement_limit' => self::BOOTSTRAP_MEASUREMENT_LIMIT,
|
||||
'snapshot_limit' => self::BOOTSTRAP_SNAPSHOT_LIMIT,
|
||||
]);
|
||||
@@ -339,25 +343,30 @@ final class Router
|
||||
'purchased_miners' => [],
|
||||
'measurement_rates' => [],
|
||||
], ['project_key' => $projectKey]);
|
||||
$measurements = $this->safeTimed('bootstrap.measurements', fn () => $this->bootstrapMeasurements($projectKey, $settings), [], [
|
||||
$measurements = $this->safeTimed('bootstrap.measurements', fn () => $this->bootstrapMeasurements($projectKey, $settings, $view), [], [
|
||||
'project_key' => $projectKey,
|
||||
'view' => $view,
|
||||
]);
|
||||
$targets = $this->safeTimed('bootstrap.targets', fn () => $this->targets($projectKey), [], [
|
||||
$targets = $this->safeTimed('bootstrap.targets', fn () => $this->bootstrapTargets($projectKey, $view), [], [
|
||||
'project_key' => $projectKey,
|
||||
'view' => $view,
|
||||
]);
|
||||
$dashboards = $this->safeTimed('bootstrap.dashboards', fn () => $this->dashboards($projectKey), [], [
|
||||
$dashboards = $this->safeTimed('bootstrap.dashboards', fn () => $this->bootstrapDashboards($projectKey, $view), [], [
|
||||
'project_key' => $projectKey,
|
||||
'view' => $view,
|
||||
]);
|
||||
$fxSnapshots = $this->safeTimed('bootstrap.fx_snapshots', fn () => $this->measurementFxSnapshots($measurements, self::BOOTSTRAP_SNAPSHOT_LIMIT), [], [
|
||||
$fxSnapshots = $this->safeTimed('bootstrap.fx_snapshots', fn () => $this->bootstrapFxSnapshots($measurements, $view), [], [
|
||||
'view' => $view,
|
||||
'measurement_count' => is_array($measurements) ? count($measurements) : 0,
|
||||
]);
|
||||
$summary = $this->safeTimed('bootstrap.summary', fn () => $this->analytics()->buildSummary($measurements, $settings, $targets), [
|
||||
$summary = $this->safeTimed('bootstrap.summary', fn () => $this->bootstrapSummary($measurements, $settings, $targets, $view), [
|
||||
'latest_measurement' => $measurements !== [] ? $measurements[array_key_last($measurements)] : null,
|
||||
'baseline' => $settings,
|
||||
'targets' => [],
|
||||
'payouts' => [],
|
||||
'miner_offers' => [],
|
||||
], [
|
||||
'view' => $view,
|
||||
'measurement_count' => is_array($measurements) ? count($measurements) : 0,
|
||||
'target_count' => is_array($targets) ? count($targets) : 0,
|
||||
]);
|
||||
@@ -381,6 +390,8 @@ final class Router
|
||||
'summary' => $summary,
|
||||
'bootstrap_meta' => [
|
||||
'degraded' => $measurementCount >= self::BOOTSTRAP_MEASUREMENT_LIMIT,
|
||||
'view' => $view,
|
||||
'overview_window_days' => self::OVERVIEW_WINDOW_DAYS,
|
||||
'measurement_limit' => self::BOOTSTRAP_MEASUREMENT_LIMIT,
|
||||
'snapshot_limit' => self::BOOTSTRAP_SNAPSHOT_LIMIT,
|
||||
'measurement_count' => $measurementCount,
|
||||
@@ -1254,17 +1265,93 @@ final class Router
|
||||
return $this->analytics()->enrichMeasurements($rows, $settings);
|
||||
}
|
||||
|
||||
private function bootstrapMeasurements(string $projectKey, array $settings): array
|
||||
private function bootstrapMeasurements(string $projectKey, array $settings, string $view): array
|
||||
{
|
||||
$rows = $this->repository()->listMeasurements($projectKey, self::BOOTSTRAP_MEASUREMENT_LIMIT);
|
||||
if (in_array($view, ['settings', 'currencies', 'dashboards'], true)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = in_array($view, ['overview', 'mining'], true)
|
||||
? $this->repository()->listRecentMeasurements($projectKey, self::BOOTSTRAP_MEASUREMENT_LIMIT)
|
||||
: $this->repository()->listMeasurements($projectKey, self::BOOTSTRAP_MEASUREMENT_LIMIT);
|
||||
|
||||
if (in_array($view, ['overview', 'mining'], true)) {
|
||||
$rows = $this->filterMeasurementsToRecentWindow($rows, self::OVERVIEW_WINDOW_DAYS);
|
||||
}
|
||||
|
||||
$this->debug->add('bootstrap.measurements.loaded', [
|
||||
'project_key' => $projectKey,
|
||||
'view' => $view,
|
||||
'row_count' => count($rows),
|
||||
'limit' => self::BOOTSTRAP_MEASUREMENT_LIMIT,
|
||||
]);
|
||||
return $this->analytics()->enrichMeasurements($rows, $settings);
|
||||
}
|
||||
|
||||
private function bootstrapTargets(string $projectKey, string $view): array
|
||||
{
|
||||
return in_array($view, ['overview', 'mining'], true) ? $this->targets($projectKey) : [];
|
||||
}
|
||||
|
||||
private function bootstrapDashboards(string $projectKey, string $view): array
|
||||
{
|
||||
return $view === 'dashboards' ? $this->dashboards($projectKey) : [];
|
||||
}
|
||||
|
||||
private function bootstrapFxSnapshots(array $measurements, string $view): array
|
||||
{
|
||||
if (!in_array($view, ['overview', 'mining', 'measurements'], true)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->measurementFxSnapshots($measurements, self::BOOTSTRAP_SNAPSHOT_LIMIT);
|
||||
}
|
||||
|
||||
private function bootstrapSummary(array $measurements, array $settings, array $targets, string $view): array
|
||||
{
|
||||
if (!in_array($view, ['overview', 'mining'], true)) {
|
||||
return [
|
||||
'latest_measurement' => $measurements !== [] ? $measurements[array_key_last($measurements)] : null,
|
||||
'baseline' => $settings,
|
||||
'targets' => [],
|
||||
'payouts' => [],
|
||||
'miner_offers' => [],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->analytics()->buildSummary($measurements, $settings, $targets);
|
||||
}
|
||||
|
||||
private function filterMeasurementsToRecentWindow(array $rows, int $windowDays): array
|
||||
{
|
||||
if ($rows === [] || $windowDays <= 0) {
|
||||
return $rows;
|
||||
}
|
||||
|
||||
$latest = $rows[array_key_last($rows)] ?? null;
|
||||
$latestTs = is_array($latest) ? strtotime((string) ($latest['measured_at'] ?? '')) : false;
|
||||
if ($latestTs === false) {
|
||||
return $rows;
|
||||
}
|
||||
|
||||
$minTs = $latestTs - ($windowDays * 86400);
|
||||
$filtered = array_values(array_filter($rows, static function (array $row) use ($minTs): bool {
|
||||
$measuredTs = strtotime((string) ($row['measured_at'] ?? ''));
|
||||
return $measuredTs !== false && $measuredTs >= $minTs;
|
||||
}));
|
||||
|
||||
$latestRow = $rows[array_key_last($rows)] ?? null;
|
||||
return $filtered !== [] || !is_array($latestRow) ? $filtered : [$latestRow];
|
||||
}
|
||||
|
||||
private function normalizeBootstrapView(string $view): string
|
||||
{
|
||||
$normalized = trim(strtolower($view));
|
||||
return in_array($normalized, ['overview', 'measurements', 'dashboards', 'currencies', 'mining', 'settings'], true)
|
||||
? $normalized
|
||||
: 'overview';
|
||||
}
|
||||
|
||||
private function createMeasurement(string $projectKey, array $input): array
|
||||
{
|
||||
$projectTimezone = $this->projectTimezone($projectKey);
|
||||
|
||||
@@ -868,50 +868,58 @@ final class AnalyticsService
|
||||
$dogePerDayPerMh = $dogePerDay / $currentHashrateMh;
|
||||
$revenue = 0.0;
|
||||
$cost = 0.0;
|
||||
$hashrateDayTotal = 0.0;
|
||||
|
||||
for ($day = 0; $day < $days; $day++) {
|
||||
$checkTs = $baseTs + ($day * 86400);
|
||||
$activeHashrate = 0.0;
|
||||
|
||||
foreach ($costPlans as $plan) {
|
||||
if (empty($plan['is_active']) || !$this->entryIsCovered($plan, $checkTs)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$activeHashrate += $this->normalizeHashrateMh($plan['mining_speed_value'] ?? null, $plan['mining_speed_unit'] ?? null);
|
||||
$activeHashrate += $this->normalizeHashrateMh($plan['bonus_speed_value'] ?? null, $plan['bonus_speed_unit'] ?? null);
|
||||
|
||||
$runtimeMonths = (int) ($plan['runtime_months'] ?? 0);
|
||||
if ($runtimeMonths > 0 && is_numeric($plan['total_cost_amount'] ?? null)) {
|
||||
$runtimeDays = $runtimeMonths * 30.4375;
|
||||
$dailyCost = $this->convertAmount((float) $plan['total_cost_amount'] / $runtimeDays, (string) ($plan['currency'] ?? ''), $currency, $latest);
|
||||
if ($dailyCost !== null) {
|
||||
$cost += $dailyCost;
|
||||
}
|
||||
}
|
||||
foreach ($costPlans as $plan) {
|
||||
if (empty($plan['is_active'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($purchasedMiners as $miner) {
|
||||
if ((array_key_exists('is_active', $miner) && empty($miner['is_active'])) || !$this->entryIsCovered($miner, $checkTs, 'purchased_at')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$activeHashrate += $this->normalizeHashrateMh($miner['mining_speed_value'] ?? null, $miner['mining_speed_unit'] ?? null);
|
||||
$activeHashrate += $this->normalizeHashrateMh($miner['bonus_speed_value'] ?? null, $miner['bonus_speed_unit'] ?? null);
|
||||
|
||||
$runtimeMonths = (int) ($miner['runtime_months'] ?? 0);
|
||||
if ($runtimeMonths > 0 && is_numeric($miner['total_cost_amount'] ?? null)) {
|
||||
$runtimeDays = $runtimeMonths * 30.4375;
|
||||
$dailyCost = $this->convertAmount((float) $miner['total_cost_amount'] / $runtimeDays, (string) ($miner['currency'] ?? ''), $currency, $latest);
|
||||
if ($dailyCost !== null) {
|
||||
$cost += $dailyCost;
|
||||
}
|
||||
}
|
||||
$coveredDays = $this->entryCoveredDayCount($plan, $baseTs, $days);
|
||||
if ($coveredDays <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$revenue += $activeHashrate * $dogePerDayPerMh * $pricePerCoin;
|
||||
$entryHashrateMh = $this->normalizeHashrateMh($plan['mining_speed_value'] ?? null, $plan['mining_speed_unit'] ?? null)
|
||||
+ $this->normalizeHashrateMh($plan['bonus_speed_value'] ?? null, $plan['bonus_speed_unit'] ?? null);
|
||||
$hashrateDayTotal += $entryHashrateMh * $coveredDays;
|
||||
|
||||
$runtimeMonths = (int) ($plan['runtime_months'] ?? 0);
|
||||
if ($runtimeMonths > 0 && is_numeric($plan['total_cost_amount'] ?? null)) {
|
||||
$runtimeDays = $runtimeMonths * 30.4375;
|
||||
$dailyCost = $this->convertAmount((float) $plan['total_cost_amount'] / $runtimeDays, (string) ($plan['currency'] ?? ''), $currency, $latest);
|
||||
if ($dailyCost !== null) {
|
||||
$cost += $dailyCost * $coveredDays;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($purchasedMiners as $miner) {
|
||||
if (array_key_exists('is_active', $miner) && empty($miner['is_active'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$coveredDays = $this->entryCoveredDayCount($miner, $baseTs, $days, 'purchased_at');
|
||||
if ($coveredDays <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entryHashrateMh = $this->normalizeHashrateMh($miner['mining_speed_value'] ?? null, $miner['mining_speed_unit'] ?? null)
|
||||
+ $this->normalizeHashrateMh($miner['bonus_speed_value'] ?? null, $miner['bonus_speed_unit'] ?? null);
|
||||
$hashrateDayTotal += $entryHashrateMh * $coveredDays;
|
||||
|
||||
$runtimeMonths = (int) ($miner['runtime_months'] ?? 0);
|
||||
if ($runtimeMonths > 0 && is_numeric($miner['total_cost_amount'] ?? null)) {
|
||||
$runtimeDays = $runtimeMonths * 30.4375;
|
||||
$dailyCost = $this->convertAmount((float) $miner['total_cost_amount'] / $runtimeDays, (string) ($miner['currency'] ?? ''), $currency, $latest);
|
||||
if ($dailyCost !== null) {
|
||||
$cost += $dailyCost * $coveredDays;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$revenue = $hashrateDayTotal * $dogePerDayPerMh * $pricePerCoin;
|
||||
|
||||
return [
|
||||
'days' => $days,
|
||||
'revenue' => $revenue,
|
||||
@@ -920,6 +928,38 @@ final class AnalyticsService
|
||||
];
|
||||
}
|
||||
|
||||
private function entryCoveredDayCount(array $entry, int $baseTs, int $days, string $startField = 'starts_at'): int
|
||||
{
|
||||
if ($days <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$runtimeMonths = (int) ($entry['runtime_months'] ?? 0);
|
||||
$startTs = $this->utcTimestamp((string) ($entry[$startField] ?? ''));
|
||||
if ($startTs <= 0) {
|
||||
return $days;
|
||||
}
|
||||
|
||||
$startIndex = max(0, (int) ceil(($startTs - $baseTs) / 86400));
|
||||
if ($startIndex >= $days) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!empty($entry['auto_renew']) || $runtimeMonths <= 0) {
|
||||
return $days - $startIndex;
|
||||
}
|
||||
|
||||
$runtimeDays = $runtimeMonths * 30.4375;
|
||||
$endTs = (int) round($startTs + ($runtimeDays * 86400));
|
||||
$endIndex = (int) floor(($endTs - $baseTs) / 86400);
|
||||
$endIndex = min($days - 1, $endIndex);
|
||||
if ($endIndex < $startIndex) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $endIndex - $startIndex + 1;
|
||||
}
|
||||
|
||||
private function utcTimestamp(?string $value): int
|
||||
{
|
||||
$normalized = trim((string) $value);
|
||||
|
||||
@@ -410,6 +410,23 @@ final class MiningRepository
|
||||
return $this->normalizeRows($stmt->fetchAll() ?: []);
|
||||
}
|
||||
|
||||
public function listRecentMeasurements(string $projectKey, int $limit = 200): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT * FROM ' . $this->table('measurements') . '
|
||||
WHERE project_key = :project_key AND owner_sub = :owner_sub
|
||||
ORDER BY measured_at DESC
|
||||
LIMIT :limit'
|
||||
);
|
||||
$stmt->bindValue(':project_key', $projectKey, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':owner_sub', $this->ownerSub, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
$rows = $this->normalizeRows($stmt->fetchAll() ?: []);
|
||||
return array_reverse($rows);
|
||||
}
|
||||
|
||||
public function listAllMeasurements(string $projectKey): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
|
||||
Reference in New Issue
Block a user