Update
This commit is contained in:
@@ -189,6 +189,21 @@
|
|||||||
}).format(Number(value));
|
}).format(Number(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deriveDailyCostAmount(totalCostAmount, runtimeMonths) {
|
||||||
|
const amount = Number(totalCostAmount);
|
||||||
|
const months = Number(runtimeMonths);
|
||||||
|
if (!Number.isFinite(amount) || !Number.isFinite(months) || months <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtimeDays = months * 30.4375;
|
||||||
|
if (!Number.isFinite(runtimeDays) || runtimeDays <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return amount / runtimeDays;
|
||||||
|
}
|
||||||
|
|
||||||
function recentMeasurementWindow(rows, windowDays) {
|
function recentMeasurementWindow(rows, windowDays) {
|
||||||
if (!Array.isArray(rows) || rows.length === 0) {
|
if (!Array.isArray(rows) || rows.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
@@ -544,8 +559,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeSchemaStatus(data) {
|
function normalizeSchemaStatus(data) {
|
||||||
const normalized = data && typeof data === 'object' ? data : {};
|
const hasPayload = !!(data && typeof data === 'object');
|
||||||
|
const normalized = hasPayload ? data : {};
|
||||||
return {
|
return {
|
||||||
|
loaded: hasPayload,
|
||||||
required_tables: Array.isArray(normalized.required_tables) ? normalized.required_tables : [],
|
required_tables: Array.isArray(normalized.required_tables) ? normalized.required_tables : [],
|
||||||
present_tables: Array.isArray(normalized.present_tables) ? normalized.present_tables : [],
|
present_tables: Array.isArray(normalized.present_tables) ? normalized.present_tables : [],
|
||||||
missing_tables: Array.isArray(normalized.missing_tables) ? normalized.missing_tables : [],
|
missing_tables: Array.isArray(normalized.missing_tables) ? normalized.missing_tables : [],
|
||||||
@@ -1275,6 +1292,14 @@
|
|||||||
baseAmount = fallbackUsdReference;
|
baseAmount = fallbackUsdReference;
|
||||||
baseCurrency = 'USD';
|
baseCurrency = 'USD';
|
||||||
}
|
}
|
||||||
|
const storedDailyAmount = Number(miner.daily_cost_amount);
|
||||||
|
const dailyAmount = Number.isFinite(storedDailyAmount) && storedDailyAmount > 0
|
||||||
|
? storedDailyAmount
|
||||||
|
: deriveDailyCostAmount(miner.total_cost_amount, miner.runtime_months);
|
||||||
|
const dailyCurrency = String(miner.daily_cost_currency || miner.currency || '').toUpperCase();
|
||||||
|
const dailyReportAmount = Number.isFinite(dailyAmount) && dailyCurrency
|
||||||
|
? (dailyCurrency === reportCurrency ? dailyAmount : convertCurrencyValue(dailyAmount, dailyCurrency, reportCurrency))
|
||||||
|
: null;
|
||||||
return {
|
return {
|
||||||
id: `purchase-${miner.id}`,
|
id: `purchase-${miner.id}`,
|
||||||
source: 'miete',
|
source: 'miete',
|
||||||
@@ -1284,6 +1309,9 @@
|
|||||||
auto_renew: effectiveAutoRenew,
|
auto_renew: effectiveAutoRenew,
|
||||||
effective_amount: miner.total_cost_amount,
|
effective_amount: miner.total_cost_amount,
|
||||||
effective_currency: miner.currency,
|
effective_currency: miner.currency,
|
||||||
|
daily_cost_amount: dailyAmount,
|
||||||
|
daily_cost_currency: dailyCurrency,
|
||||||
|
daily_cost_report_amount: Number.isFinite(dailyReportAmount) ? dailyReportAmount : null,
|
||||||
base_amount: baseAmount,
|
base_amount: baseAmount,
|
||||||
base_currency: baseCurrency,
|
base_currency: baseCurrency,
|
||||||
miner_id: miner.id,
|
miner_id: miner.id,
|
||||||
@@ -1299,6 +1327,14 @@
|
|||||||
};
|
};
|
||||||
}).concat(currentCostPlans.map((plan) => {
|
}).concat(currentCostPlans.map((plan) => {
|
||||||
const coverage = entryCoverageMeta(plan.starts_at, plan.runtime_months, plan.auto_renew);
|
const coverage = entryCoverageMeta(plan.starts_at, plan.runtime_months, plan.auto_renew);
|
||||||
|
const storedDailyAmount = Number(plan.daily_cost_amount);
|
||||||
|
const dailyAmount = Number.isFinite(storedDailyAmount) && storedDailyAmount > 0
|
||||||
|
? storedDailyAmount
|
||||||
|
: deriveDailyCostAmount(plan.total_cost_amount, plan.runtime_months);
|
||||||
|
const dailyCurrency = String(plan.daily_cost_currency || plan.currency || '').toUpperCase();
|
||||||
|
const dailyReportAmount = Number.isFinite(dailyAmount) && dailyCurrency
|
||||||
|
? (dailyCurrency === reportCurrency ? dailyAmount : convertCurrencyValue(dailyAmount, dailyCurrency, reportCurrency))
|
||||||
|
: null;
|
||||||
return {
|
return {
|
||||||
id: `plan-${plan.id}`,
|
id: `plan-${plan.id}`,
|
||||||
source: 'manual',
|
source: 'manual',
|
||||||
@@ -1308,6 +1344,9 @@
|
|||||||
auto_renew: !!plan.auto_renew,
|
auto_renew: !!plan.auto_renew,
|
||||||
effective_amount: plan.total_cost_amount,
|
effective_amount: plan.total_cost_amount,
|
||||||
effective_currency: plan.currency,
|
effective_currency: plan.currency,
|
||||||
|
daily_cost_amount: dailyAmount,
|
||||||
|
daily_cost_currency: dailyCurrency,
|
||||||
|
daily_cost_report_amount: Number.isFinite(dailyReportAmount) ? dailyReportAmount : null,
|
||||||
base_amount: plan.base_price_amount,
|
base_amount: plan.base_price_amount,
|
||||||
base_currency: currentSettings.report_currency || 'EUR',
|
base_currency: currentSettings.report_currency || 'EUR',
|
||||||
payment_type: plan.payment_type,
|
payment_type: plan.payment_type,
|
||||||
@@ -1642,6 +1681,7 @@
|
|||||||
setSchemaStatus(normalizeSchemaStatus(schema));
|
setSchemaStatus(normalizeSchemaStatus(schema));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setSchemaStatus(normalizeSchemaStatus(null));
|
setSchemaStatus(normalizeSchemaStatus(null));
|
||||||
|
setError((previous) => previous || `Schema-Status konnte nicht geladen werden: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2984,6 +3024,13 @@
|
|||||||
const walletBalanceCurrentAsset = payload?.summary?.payouts?.wallet_balance_current_asset;
|
const walletBalanceCurrentAsset = payload?.summary?.payouts?.wallet_balance_current_asset;
|
||||||
const walletBalanceCurrentAssetDirect = payload?.summary?.payouts?.wallet_balance_current_asset_direct;
|
const walletBalanceCurrentAssetDirect = payload?.summary?.payouts?.wallet_balance_current_asset_direct;
|
||||||
const holdingsCurrentAsset = payload?.summary?.payouts?.holdings_current_asset;
|
const holdingsCurrentAsset = payload?.summary?.payouts?.holdings_current_asset;
|
||||||
|
const minerVisibleCoins = latest ? Number(latest.coins_total_visible ?? latest.coins_total) : null;
|
||||||
|
const minerValueUsd = latest && minerVisibleCoins !== null
|
||||||
|
? convertMeasurementMoney(latest, minerVisibleCoins, 'USD')
|
||||||
|
: null;
|
||||||
|
const minerValueReport = latest && minerVisibleCoins !== null
|
||||||
|
? convertMeasurementMoney(latest, minerVisibleCoins, reportCurrency)
|
||||||
|
: null;
|
||||||
|
|
||||||
return h('div', { className: 'mc-stack' }, [
|
return h('div', { className: 'mc-stack' }, [
|
||||||
panel('Berichtswährung', 'Bestimmt die Währung für Kennzahlen im Überblick. Standard kommt aus den Settings, diese Auswahl gilt nur für den aktuellen Besuch.', [
|
panel('Berichtswährung', 'Bestimmt die Währung für Kennzahlen im Überblick. Standard kommt aus den Settings, diese Auswahl gilt nur für den aktuellen Besuch.', [
|
||||||
@@ -3006,8 +3053,12 @@
|
|||||||
h(StatCard, {
|
h(StatCard, {
|
||||||
key: 'coins',
|
key: 'coins',
|
||||||
label: `${currentCoinCurrency} im Miner`,
|
label: `${currentCoinCurrency} im Miner`,
|
||||||
value: latest ? fmtNumber(latest.coins_total_visible ?? latest.coins_total, 6) : 'n/a',
|
value: latest ? fmtNumber(minerVisibleCoins, 6) : 'n/a',
|
||||||
sub: latest ? `Stand ${fmtDate(latest.measured_at)}` : '',
|
sub: latest ? [
|
||||||
|
minerValueUsd !== null ? `USD ${fmtMoney(minerValueUsd, 'USD')}` : null,
|
||||||
|
reportCurrency !== 'USD' && minerValueReport !== null ? `${reportCurrency} ${fmtMoney(minerValueReport, reportCurrency)}` : null,
|
||||||
|
`Stand ${fmtDate(latest.measured_at)}`,
|
||||||
|
].filter(Boolean).join(' · ') : '',
|
||||||
}),
|
}),
|
||||||
h(StatCard, {
|
h(StatCard, {
|
||||||
key: 'earned-overall',
|
key: 'earned-overall',
|
||||||
@@ -3434,6 +3485,14 @@
|
|||||||
h('td', { key: 'renew' }, row.payment_type === 'crypto' ? 'nein' : (row.auto_renew ? 'ja' : 'nein')),
|
h('td', { key: 'renew' }, row.payment_type === 'crypto' ? 'nein' : (row.auto_renew ? 'ja' : 'nein')),
|
||||||
h('td', { key: 'cost' }, [
|
h('td', { key: 'cost' }, [
|
||||||
h('div', { key: 'effective' }, fmtNumber(row.effective_amount, 6)),
|
h('div', { key: 'effective' }, fmtNumber(row.effective_amount, 6)),
|
||||||
|
row.daily_cost_amount !== null && row.daily_cost_amount !== undefined && row.daily_cost_currency
|
||||||
|
? h('div', { key: 'daily', className: 'mc-kicker' }, [
|
||||||
|
`Pro Tag ${fmtNumber(row.daily_cost_amount, 6)} ${row.daily_cost_currency}`,
|
||||||
|
row.daily_cost_report_amount !== null && row.daily_cost_report_amount !== undefined && row.daily_cost_currency !== reportCurrency
|
||||||
|
? ` · ${fmtMoney(row.daily_cost_report_amount, reportCurrency)}`
|
||||||
|
: '',
|
||||||
|
].join(''))
|
||||||
|
: null,
|
||||||
row.base_amount !== null && row.base_amount !== undefined && row.base_currency
|
row.base_amount !== null && row.base_amount !== undefined && row.base_currency
|
||||||
? h('div', { key: 'base', className: 'mc-kicker' }, `Basis ${fmtNumber(row.base_amount, 6)} ${row.base_currency}`)
|
? h('div', { key: 'base', className: 'mc-kicker' }, `Basis ${fmtNumber(row.base_amount, 6)} ${row.base_currency}`)
|
||||||
: null,
|
: null,
|
||||||
@@ -3576,12 +3635,24 @@
|
|||||||
offer.crypto_display_price_amount !== null && offer.crypto_display_price_currency
|
offer.crypto_display_price_amount !== null && offer.crypto_display_price_currency
|
||||||
? h('div', { key: 'price-crypto' }, [
|
? h('div', { key: 'price-crypto' }, [
|
||||||
h('div', { key: 'amount' }, `${fmtNumber(offer.crypto_display_price_amount, 6)} ${offer.crypto_display_price_currency}`),
|
h('div', { key: 'amount' }, `${fmtNumber(offer.crypto_display_price_amount, 6)} ${offer.crypto_display_price_currency}`),
|
||||||
|
(() => {
|
||||||
|
const offerDailyAmount = deriveDailyCostAmount(offer.crypto_display_price_amount, offer.runtime_months);
|
||||||
|
return offerDailyAmount !== null
|
||||||
|
? h('div', { key: 'daily', className: 'mc-kicker' }, `Pro Tag ${fmtNumber(offerDailyAmount, 6)} ${offer.crypto_display_price_currency}`)
|
||||||
|
: null;
|
||||||
|
})(),
|
||||||
h('div', { key: 'label', className: 'mc-kicker' }, 'Zu zahlen in Krypto'),
|
h('div', { key: 'label', className: 'mc-kicker' }, 'Zu zahlen in Krypto'),
|
||||||
])
|
])
|
||||||
: null,
|
: null,
|
||||||
offer.usd_display_price_amount !== null && offer.usd_display_price_currency
|
offer.usd_display_price_amount !== null && offer.usd_display_price_currency
|
||||||
? h('div', { key: 'price-usd' }, [
|
? h('div', { key: 'price-usd' }, [
|
||||||
h('div', { key: 'amount' }, `${fmtNumber(offer.usd_display_price_amount, 6)} ${offer.usd_display_price_currency}`),
|
h('div', { key: 'amount' }, `${fmtNumber(offer.usd_display_price_amount, 6)} ${offer.usd_display_price_currency}`),
|
||||||
|
(() => {
|
||||||
|
const offerDailyAmount = deriveDailyCostAmount(offer.usd_display_price_amount, offer.runtime_months);
|
||||||
|
return offerDailyAmount !== null
|
||||||
|
? h('div', { key: 'daily', className: 'mc-kicker' }, `Pro Tag ${fmtNumber(offerDailyAmount, 6)} ${offer.usd_display_price_currency}`)
|
||||||
|
: null;
|
||||||
|
})(),
|
||||||
h('div', { key: 'label', className: 'mc-kicker' }, 'Zu zahlen in USD'),
|
h('div', { key: 'label', className: 'mc-kicker' }, 'Zu zahlen in USD'),
|
||||||
])
|
])
|
||||||
: null,
|
: null,
|
||||||
@@ -3589,6 +3660,12 @@
|
|||||||
: [
|
: [
|
||||||
h('div', { key: 'price-main' }, [
|
h('div', { key: 'price-main' }, [
|
||||||
h('div', { key: 'amount' }, `${fmtNumber(offer.effective_price_amount, 6)} ${offer.effective_price_currency}`),
|
h('div', { key: 'amount' }, `${fmtNumber(offer.effective_price_amount, 6)} ${offer.effective_price_currency}`),
|
||||||
|
(() => {
|
||||||
|
const offerDailyAmount = deriveDailyCostAmount(offer.effective_price_amount, offer.runtime_months);
|
||||||
|
return offerDailyAmount !== null
|
||||||
|
? h('div', { key: 'daily', className: 'mc-kicker' }, `Pro Tag ${fmtNumber(offerDailyAmount, 6)} ${offer.effective_price_currency}`)
|
||||||
|
: null;
|
||||||
|
})(),
|
||||||
h('div', { key: 'label', className: 'mc-kicker' }, 'Zu zahlen'),
|
h('div', { key: 'label', className: 'mc-kicker' }, 'Zu zahlen'),
|
||||||
]),
|
]),
|
||||||
offer.base_price_amount !== null && offer.base_price_currency
|
offer.base_price_amount !== null && offer.base_price_currency
|
||||||
@@ -3892,10 +3969,10 @@
|
|||||||
h('div', { className: 'mc-stack' }, [
|
h('div', { className: 'mc-stack' }, [
|
||||||
panel('Initialisierung', 'Prueft den Tabellenstatus und kann das Mining-Checker Schema neu anlegen. Reset loescht bestehende miningcheck_ Tabellen inklusive Daten.', [
|
panel('Initialisierung', 'Prueft den Tabellenstatus und kann das Mining-Checker Schema neu anlegen. Reset loescht bestehende miningcheck_ Tabellen inklusive Daten.', [
|
||||||
h('div', { key: 'status', className: 'mc-form' }, [
|
h('div', { key: 'status', className: 'mc-form' }, [
|
||||||
displayField('Status', schemaStatus.all_present ? 'Schema vollstaendig vorhanden' : 'Schema unvollstaendig'),
|
displayField('Status', !schemaStatus.loaded ? 'Status unbekannt' : (schemaStatus.all_present ? 'Schema vollstaendig vorhanden' : 'Schema unvollstaendig')),
|
||||||
displayField('Vorhandene Tabellen', `${schemaStatus.present_count}/${schemaStatus.required_tables.length}`),
|
displayField('Vorhandene Tabellen', schemaStatus.loaded ? `${schemaStatus.present_count}/${schemaStatus.required_tables.length}` : 'Status konnte nicht geladen werden'),
|
||||||
displayField('Fehlende Tabellen', schemaStatus.missing_tables.length ? schemaStatus.missing_tables.join(', ') : 'keine'),
|
displayField('Fehlende Tabellen', schemaStatus.loaded ? (schemaStatus.missing_tables.length ? schemaStatus.missing_tables.join(', ') : 'keine') : 'Status konnte nicht geladen werden'),
|
||||||
displayField('Ausstehende Upgrades', schemaStatus.pending_upgrades.length ? schemaStatus.pending_upgrades.join(', ') : 'keine'),
|
displayField('Ausstehende Upgrades', schemaStatus.loaded ? (schemaStatus.pending_upgrades.length ? schemaStatus.pending_upgrades.join(', ') : 'keine') : 'Status konnte nicht geladen werden'),
|
||||||
]),
|
]),
|
||||||
h('form', { key: 'form', className: 'mc-form', onSubmit: initializeModule }, [
|
h('form', { key: 'form', className: 'mc-form', onSubmit: initializeModule }, [
|
||||||
h('label', { className: 'mc-checkbox' }, [
|
h('label', { className: 'mc-checkbox' }, [
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Modules\MiningChecker\Infrastructure;
|
namespace Modules\MiningChecker\Infrastructure;
|
||||||
|
|
||||||
use App\SqlDataImporter;
|
|
||||||
use App\UploadedSqlFile;
|
|
||||||
use Modules\MiningChecker\Support\ApiException;
|
use Modules\MiningChecker\Support\ApiException;
|
||||||
|
use Modules\MiningChecker\Support\SqlDataImporter;
|
||||||
|
use Modules\MiningChecker\Support\UploadedSqlFile;
|
||||||
use PDO;
|
use PDO;
|
||||||
|
|
||||||
final class SchemaManager
|
final class SchemaManager
|
||||||
|
|||||||
293
custom/apps/mining-checker/src/Support/SqlDataImporter.php
Normal file
293
custom/apps/mining-checker/src/Support/SqlDataImporter.php
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Modules\MiningChecker\Support;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
final class SqlDataImporter
|
||||||
|
{
|
||||||
|
private PDO $pdo;
|
||||||
|
private string $driver;
|
||||||
|
|
||||||
|
public function __construct(PDO $pdo)
|
||||||
|
{
|
||||||
|
$this->pdo = $pdo;
|
||||||
|
$this->driver = strtolower((string) $pdo->getAttribute(PDO::ATTR_DRIVER_NAME));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function importString(string $sql): int
|
||||||
|
{
|
||||||
|
$statements = [];
|
||||||
|
foreach ($this->splitStatements($sql) as $statement) {
|
||||||
|
$trimmed = trim($statement);
|
||||||
|
if ($trimmed === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$statements[] = $this->normalizeImportStatement($trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->executeStatements($statements);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $statements
|
||||||
|
*/
|
||||||
|
private function executeStatements(array $statements): int
|
||||||
|
{
|
||||||
|
$useTransaction = $this->driver === 'pgsql' && !$this->pdo->inTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($useTransaction) {
|
||||||
|
$this->pdo->beginTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
$executed = $this->executeImportPass($statements);
|
||||||
|
|
||||||
|
if ($useTransaction && $this->pdo->inTransaction()) {
|
||||||
|
$this->pdo->commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $executed;
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
if ($useTransaction && $this->pdo->inTransaction()) {
|
||||||
|
$this->pdo->rollBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $statements
|
||||||
|
*/
|
||||||
|
private function executeImportPass(array $statements): int
|
||||||
|
{
|
||||||
|
$pendingStatements = $statements;
|
||||||
|
$executed = 0;
|
||||||
|
$maxPasses = max(1, count($pendingStatements));
|
||||||
|
|
||||||
|
for ($pass = 0; $pass < $maxPasses && $pendingStatements !== []; $pass++) {
|
||||||
|
$deferredStatements = [];
|
||||||
|
$progressMade = false;
|
||||||
|
|
||||||
|
foreach ($pendingStatements as $statement) {
|
||||||
|
try {
|
||||||
|
$this->execStatementWithRecovery($statement);
|
||||||
|
$executed++;
|
||||||
|
$progressMade = true;
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
if ($this->shouldRetryDeferredImportStatement($exception, $statement)) {
|
||||||
|
$deferredStatements[] = $statement;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \RuntimeException($statement, 0, $exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($deferredStatements === []) {
|
||||||
|
return $executed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$progressMade) {
|
||||||
|
try {
|
||||||
|
$this->execStatementWithRecovery($deferredStatements[0]);
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
throw new \RuntimeException($deferredStatements[0], 0, $exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pendingStatements = $deferredStatements;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $executed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function execStatementWithRecovery(string $statement): void
|
||||||
|
{
|
||||||
|
if ($this->driver !== 'pgsql' || !$this->pdo->inTransaction()) {
|
||||||
|
$this->pdo->exec($statement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$savepoint = 'sql_import_' . substr(sha1($statement . microtime(true)), 0, 12);
|
||||||
|
$this->pdo->exec('SAVEPOINT ' . $savepoint);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->pdo->exec($statement);
|
||||||
|
$this->pdo->exec('RELEASE SAVEPOINT ' . $savepoint);
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
try {
|
||||||
|
$this->pdo->exec('ROLLBACK TO SAVEPOINT ' . $savepoint);
|
||||||
|
$this->pdo->exec('RELEASE SAVEPOINT ' . $savepoint);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shouldRetryDeferredImportStatement(\Throwable $exception, string $statement): bool
|
||||||
|
{
|
||||||
|
if ($this->driver !== 'pgsql') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match('/^(INSERT|COPY)\b/i', ltrim($statement))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $exception->getCode() === '23503';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeImportStatement(string $statement): string
|
||||||
|
{
|
||||||
|
if ($this->driver !== 'pgsql') {
|
||||||
|
return $statement;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedSetval = $this->normalizePgsqlSetvalStatement($statement);
|
||||||
|
return $resolvedSetval ?? $statement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizePgsqlSetvalStatement(string $statement): ?string
|
||||||
|
{
|
||||||
|
$pattern = "/^SELECT\\s+setval\\(\\s*'([^']+)'\\s*,\\s*([0-9]+)\\s*,\\s*(true|false)\\s*\\)$/i";
|
||||||
|
if (!preg_match($pattern, trim($statement), $matches)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sequenceReference = $matches[1];
|
||||||
|
$nextValue = $matches[2];
|
||||||
|
$isCalled = strtolower($matches[3]);
|
||||||
|
|
||||||
|
if ($this->pgsqlRelationExists($sequenceReference)) {
|
||||||
|
return $statement;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sequenceName = $sequenceReference;
|
||||||
|
$schemaName = 'public';
|
||||||
|
if (str_contains($sequenceReference, '.')) {
|
||||||
|
[$schemaName, $sequenceName] = explode('.', $sequenceReference, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$schemaName = trim($schemaName, "\"'");
|
||||||
|
$sequenceName = trim($sequenceName, "\"'");
|
||||||
|
|
||||||
|
if (!preg_match('/^(.*)_([A-Za-z0-9]+)_seq\d*$/', $sequenceName, $parts)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tableName = $parts[1];
|
||||||
|
$columnName = $parts[2];
|
||||||
|
$actualSequence = $this->resolvePgsqlSerialSequence($schemaName, $tableName, $columnName);
|
||||||
|
if ($actualSequence === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf("SELECT setval('%s', %s, %s)", $actualSequence, $nextValue, $isCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pgsqlRelationExists(string $qualifiedName): bool
|
||||||
|
{
|
||||||
|
$statement = $this->pdo->prepare('SELECT to_regclass(:name) IS NOT NULL');
|
||||||
|
$statement->execute(['name' => $qualifiedName]);
|
||||||
|
return (bool) $statement->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolvePgsqlSerialSequence(string $schemaName, string $tableName, string $columnName): ?string
|
||||||
|
{
|
||||||
|
$qualifiedTable = sprintf('"%s"."%s"', str_replace('"', '""', $schemaName), str_replace('"', '""', $tableName));
|
||||||
|
$statement = $this->pdo->prepare('SELECT pg_get_serial_sequence(:table_name, :column_name)');
|
||||||
|
$statement->execute([
|
||||||
|
'table_name' => $qualifiedTable,
|
||||||
|
'column_name' => $columnName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $statement->fetchColumn();
|
||||||
|
return is_string($result) && $result !== '' ? $result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function splitStatements(string $sql): array
|
||||||
|
{
|
||||||
|
$statements = [];
|
||||||
|
$buffer = '';
|
||||||
|
$length = strlen($sql);
|
||||||
|
$inSingleQuote = false;
|
||||||
|
$inDoubleQuote = false;
|
||||||
|
$inBacktickQuote = false;
|
||||||
|
$inLineComment = false;
|
||||||
|
$inBlockComment = false;
|
||||||
|
|
||||||
|
for ($index = 0; $index < $length; $index++) {
|
||||||
|
$char = $sql[$index];
|
||||||
|
$next = $index + 1 < $length ? $sql[$index + 1] : '';
|
||||||
|
|
||||||
|
if ($inLineComment) {
|
||||||
|
if ($char === "\n") {
|
||||||
|
$inLineComment = false;
|
||||||
|
$buffer .= $char;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($inBlockComment) {
|
||||||
|
if ($char === '*' && $next === '/') {
|
||||||
|
$inBlockComment = false;
|
||||||
|
$index++;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$inSingleQuote && !$inDoubleQuote && !$inBacktickQuote) {
|
||||||
|
if ($char === '-' && $next === '-') {
|
||||||
|
$inLineComment = true;
|
||||||
|
$index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($char === '#') {
|
||||||
|
$inLineComment = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($char === '/' && $next === '*') {
|
||||||
|
$inBlockComment = true;
|
||||||
|
$index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($char === "'" && !$inDoubleQuote && !$inBacktickQuote) {
|
||||||
|
$escaped = $index > 0 && $sql[$index - 1] === '\\';
|
||||||
|
if (!$escaped) {
|
||||||
|
$inSingleQuote = !$inSingleQuote;
|
||||||
|
}
|
||||||
|
} elseif ($char === '"' && !$inSingleQuote && !$inBacktickQuote) {
|
||||||
|
$escaped = $index > 0 && $sql[$index - 1] === '\\';
|
||||||
|
if (!$escaped) {
|
||||||
|
$inDoubleQuote = !$inDoubleQuote;
|
||||||
|
}
|
||||||
|
} elseif ($char === '`' && !$inSingleQuote && !$inDoubleQuote) {
|
||||||
|
$inBacktickQuote = !$inBacktickQuote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($char === ';' && !$inSingleQuote && !$inDoubleQuote && !$inBacktickQuote) {
|
||||||
|
$statements[] = $buffer;
|
||||||
|
$buffer = '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$buffer .= $char;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trim($buffer) !== '') {
|
||||||
|
$statements[] = $buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $statements;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
custom/apps/mining-checker/src/Support/UploadedSqlFile.php
Normal file
35
custom/apps/mining-checker/src/Support/UploadedSqlFile.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Modules\MiningChecker\Support;
|
||||||
|
|
||||||
|
final class UploadedSqlFile
|
||||||
|
{
|
||||||
|
public static function read(array $uploadedFile): array
|
||||||
|
{
|
||||||
|
$errorCode = (int) ($uploadedFile['error'] ?? UPLOAD_ERR_NO_FILE);
|
||||||
|
if ($errorCode !== UPLOAD_ERR_OK) {
|
||||||
|
throw new \RuntimeException('SQL-Datei konnte nicht hochgeladen werden. Upload-Fehler: ' . $errorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalName = (string) ($uploadedFile['name'] ?? 'import.sql');
|
||||||
|
$tmpPath = (string) ($uploadedFile['tmp_name'] ?? '');
|
||||||
|
if ($tmpPath === '' || !is_uploaded_file($tmpPath)) {
|
||||||
|
throw new \RuntimeException('Ungueltige Upload-Datei fuer SQL-Import.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match('/\.sql$/i', $originalName)) {
|
||||||
|
throw new \RuntimeException('Bitte eine SQL-Datei mit Endung .sql hochladen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = @file_get_contents($tmpPath);
|
||||||
|
if (!is_string($sql) || trim($sql) === '') {
|
||||||
|
throw new \RuntimeException('Die hochgeladene SQL-Datei ist leer oder konnte nicht gelesen werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'file' => $originalName,
|
||||||
|
'sql' => $sql,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1671,18 +1671,28 @@ body .tray-pill.tray-pill-status--unconfigured {
|
|||||||
|
|
||||||
.window {
|
.window {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
left: 0 !important;
|
|
||||||
top: 0 !important;
|
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.window.is-mobile-fullscreen {
|
||||||
|
left: 0 !important;
|
||||||
|
border-radius: 0;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window.is-mobile-fullscreen .window-header {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window.is-mobile-fullscreen .window-resize-handle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.window-header {
|
.window-header {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1171,6 +1171,8 @@ if (payloadNode) {
|
|||||||
openDebugWindow();
|
openDebugWindow();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isMobileViewport = () => window.matchMedia('(max-width: 1100px)').matches;
|
||||||
|
|
||||||
const applyWindowLayout = (record) => {
|
const applyWindowLayout = (record) => {
|
||||||
const { x, y, width, height } = record.layout;
|
const { x, y, width, height } = record.layout;
|
||||||
record.node.style.left = `${x}px`;
|
record.node.style.left = `${x}px`;
|
||||||
@@ -1179,6 +1181,35 @@ if (payloadNode) {
|
|||||||
record.node.style.height = `${height}px`;
|
record.node.style.height = `${height}px`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applyManagedWindowFrame = (record) => {
|
||||||
|
if (isMobileViewport()) {
|
||||||
|
const mobileTopInset = maximizeOverSystemBar ? 0 : topDesktopInset;
|
||||||
|
record.node.classList.add('is-mobile-fullscreen');
|
||||||
|
record.node.style.left = '0px';
|
||||||
|
record.node.style.top = `${mobileTopInset}px`;
|
||||||
|
record.node.style.width = '100%';
|
||||||
|
record.node.style.height = maximizeOverSystemBar
|
||||||
|
? `calc(100% - ${bottomDesktopInset}px)`
|
||||||
|
: `calc(100% - ${mobileTopInset + bottomDesktopInset}px)`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
record.node.classList.remove('is-mobile-fullscreen');
|
||||||
|
|
||||||
|
if (record.state.maximized) {
|
||||||
|
const maximizedTopInset = maximizeOverSystemBar ? 0 : topDesktopInset;
|
||||||
|
record.node.style.left = `${sideDesktopInset}px`;
|
||||||
|
record.node.style.top = `${maximizedTopInset}px`;
|
||||||
|
record.node.style.width = sideDesktopInset === 0 ? '100%' : `calc(100% - ${sideDesktopInset * 2}px)`;
|
||||||
|
record.node.style.height = maximizeOverSystemBar
|
||||||
|
? '100%'
|
||||||
|
: `calc(100% - ${maximizedTopInset + bottomDesktopInset}px)`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyWindowLayout(record);
|
||||||
|
};
|
||||||
|
|
||||||
const clampWindowLayout = (record) => {
|
const clampWindowLayout = (record) => {
|
||||||
const minX = 0;
|
const minX = 0;
|
||||||
const minY = maximizeOverSystemBar ? 0 : topDesktopInset;
|
const minY = maximizeOverSystemBar ? 0 : topDesktopInset;
|
||||||
@@ -1218,17 +1249,17 @@ if (payloadNode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!record.state.maximized) {
|
if (!record.state.maximized) {
|
||||||
|
if (isMobileViewport()) {
|
||||||
|
focusWindow(windowId);
|
||||||
|
saveWindowState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
record.restoreLayout = { ...record.layout };
|
record.restoreLayout = { ...record.layout };
|
||||||
record.state.maximized = true;
|
record.state.maximized = true;
|
||||||
record.node.classList.add('is-maximized');
|
record.node.classList.add('is-maximized');
|
||||||
measureDesktopBounds();
|
measureDesktopBounds();
|
||||||
const maximizedTopInset = maximizeOverSystemBar ? 0 : topDesktopInset;
|
applyManagedWindowFrame(record);
|
||||||
record.node.style.left = `${sideDesktopInset}px`;
|
|
||||||
record.node.style.top = `${maximizedTopInset}px`;
|
|
||||||
record.node.style.width = sideDesktopInset === 0 ? '100%' : `calc(100% - ${sideDesktopInset * 2}px)`;
|
|
||||||
record.node.style.height = maximizeOverSystemBar
|
|
||||||
? '100%'
|
|
||||||
: `calc(100% - ${maximizedTopInset + bottomDesktopInset}px)`;
|
|
||||||
focusWindow(windowId);
|
focusWindow(windowId);
|
||||||
saveWindowState();
|
saveWindowState();
|
||||||
return;
|
return;
|
||||||
@@ -1239,7 +1270,7 @@ if (payloadNode) {
|
|||||||
if (record.restoreLayout) {
|
if (record.restoreLayout) {
|
||||||
record.layout = { ...record.restoreLayout };
|
record.layout = { ...record.restoreLayout };
|
||||||
clampWindowLayout(record);
|
clampWindowLayout(record);
|
||||||
applyWindowLayout(record);
|
applyManagedWindowFrame(record);
|
||||||
}
|
}
|
||||||
focusWindow(windowId);
|
focusWindow(windowId);
|
||||||
saveWindowState();
|
saveWindowState();
|
||||||
@@ -1537,14 +1568,8 @@ if (payloadNode) {
|
|||||||
if (record.state.maximized) {
|
if (record.state.maximized) {
|
||||||
record.node.classList.add('is-maximized');
|
record.node.classList.add('is-maximized');
|
||||||
measureDesktopBounds();
|
measureDesktopBounds();
|
||||||
const maximizedTopInset = maximizeOverSystemBar ? 0 : topDesktopInset;
|
|
||||||
record.node.style.left = `${sideDesktopInset}px`;
|
|
||||||
record.node.style.top = `${maximizedTopInset}px`;
|
|
||||||
record.node.style.width = sideDesktopInset === 0 ? '100%' : `calc(100% - ${sideDesktopInset * 2}px)`;
|
|
||||||
record.node.style.height = maximizeOverSystemBar
|
|
||||||
? '100%'
|
|
||||||
: `calc(100% - ${maximizedTopInset + bottomDesktopInset}px)`;
|
|
||||||
}
|
}
|
||||||
|
applyManagedWindowFrame(record);
|
||||||
|
|
||||||
node.addEventListener('mousedown', () => focusWindow(record.id));
|
node.addEventListener('mousedown', () => focusWindow(record.id));
|
||||||
|
|
||||||
@@ -2263,19 +2288,7 @@ if (payloadNode) {
|
|||||||
|
|
||||||
windows.forEach((record) => {
|
windows.forEach((record) => {
|
||||||
clampWindowLayout(record);
|
clampWindowLayout(record);
|
||||||
|
applyManagedWindowFrame(record);
|
||||||
if (record.state.maximized) {
|
|
||||||
const maximizedTopInset = maximizeOverSystemBar ? 0 : topDesktopInset;
|
|
||||||
record.node.style.left = `${sideDesktopInset}px`;
|
|
||||||
record.node.style.top = `${maximizedTopInset}px`;
|
|
||||||
record.node.style.width = sideDesktopInset === 0 ? '100%' : `calc(100% - ${sideDesktopInset * 2}px)`;
|
|
||||||
record.node.style.height = maximizeOverSystemBar
|
|
||||||
? '100%'
|
|
||||||
: `calc(100% - ${maximizedTopInset + bottomDesktopInset}px)`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
applyWindowLayout(record);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
saveWindowState();
|
saveWindowState();
|
||||||
|
|||||||
Reference in New Issue
Block a user