diff --git a/android/adb/AdbUtils.cpp b/android/adb/AdbUtils.cpp
index 3b697bd..9aa7c52 100644
--- a/android/adb/AdbUtils.cpp
+++ b/android/adb/AdbUtils.cpp
@@ -816,6 +816,33 @@ QString AdbUtils::getForegroundPackage(const QString &deviceId) {
return {};
}
+bool AdbUtils::ensurePortraitOrientation(const QString &deviceId) {
+ if (deviceId.isEmpty()) {
+ return false;
+ }
+ const QString adb = adbPath();
+ QString out;
+ if (!startProcess(adb, {"-s", deviceId, "shell", "dumpsys", "input"}, &out)) {
+ return false; // adb недоступен — пропускаем (best-effort)
+ }
+
+ // SurfaceOrientation: 0=0°(portrait) 1=90°(landscape) 2=180° 3=270°(landscape)
+ static const QRegularExpression re(R"(SurfaceOrientation:\s*(\d))");
+ const QRegularExpressionMatch m = re.match(out);
+ const int orient = m.hasMatch() ? m.captured(1).toInt() : 0;
+
+ if (orient == 1 || orient == 3) {
+ qInfo() << "[AdbUtils] device" << deviceId
+ << "in landscape (orient" << orient << ") → forcing portrait";
+ // Автоповорот выключаем, иначе сенсор вернёт landscape.
+ startProcess(adb, {"-s", deviceId, "shell", "settings", "put",
+ "system", "accelerometer_rotation", "0"});
+ startProcess(adb, {"-s", deviceId, "shell", "settings", "put",
+ "system", "user_rotation", "0"});
+ }
+ return true;
+}
+
void AdbUtils::unlockScreen(const QString &deviceId, const int screenWidth, const int screenHeight) {
const QString adb = adbPath();
diff --git a/android/adb/AdbUtils.h b/android/adb/AdbUtils.h
index 52dd923..8113c49 100644
--- a/android/adb/AdbUtils.h
+++ b/android/adb/AdbUtils.h
@@ -62,6 +62,13 @@ public:
// Возвращает пакет приложения на переднем плане (foreground), либо пустую строку при ошибке
static QString getForegroundPackage(const QString &deviceId);
+ // Проверяет реальную ориентацию экрана (dumpsys input → SurfaceOrientation).
+ // Если экран в горизонтальной ориентации (landscape, 90°/270°), принудительно
+ // ставит вертикальную: выключает автоповорот и выставляет user_rotation=0.
+ // Best-effort: при ошибке adb просто ничего не делает. Возвращает true, если
+ // экран в портрете (изначально или после форсирования).
+ static bool ensurePortraitOrientation(const QString &deviceId);
+
private:
static QString adbDir();
static bool startProcess(const QString &program, const QStringList &args,
diff --git a/android/dushanbe/GetLastTransactionsScript.cpp b/android/dushanbe/GetLastTransactionsScript.cpp
index 18a46f0..439857e 100644
--- a/android/dushanbe/GetLastTransactionsScript.cpp
+++ b/android/dushanbe/GetLastTransactionsScript.cpp
@@ -719,14 +719,16 @@ void GetLastTransactionsScript::searchAll(
const double absListAmt = qAbs(tx.amount);
// Выбираем подходящую цель: совпадение по сумме + пред-фильтры даты/времени.
+ // Список Выписки, экран деталей и поисковый таргет — все в мажорных
+ // единицах (TJS, 2 знака), поэтому сверяем сумму напрямую. Эвристик
+ // "x100" убран: он ложно матчил реальную транзакцию на 1.00 к поиску
+ // на 100.00 (1.00*100==100.00) и затем домножал результат до 10000.
int matchIdx = -1;
- bool matchedX100 = false;
for (int ti = 0; ti < pending.size(); ++ti) {
const double searchAbs = qAbs(pending[ti].first);
const QDateTime &cand = pending[ti].second;
const bool amountDirect = qAbs(absListAmt - searchAbs) < 0.01;
- const bool amountX100 = qAbs(absListAmt * 100.0 - searchAbs) < 0.01;
- if (!amountDirect && !amountX100) continue;
+ if (!amountDirect) continue;
// Pre-filter по ДАТЕ: только если дата надёжная (из content-desc P2P формата).
// DCWallet транзакции получают дату из groupDates ImageView, которая
@@ -753,14 +755,12 @@ void GetLastTransactionsScript::searchAll(
}
matchIdx = ti;
- matchedX100 = amountX100 && !amountDirect;
break;
}
if (matchIdx < 0) continue;
const QDateTime searchTime = pending[matchIdx].second;
qDebug() << "[Dushanbe::GetLastTransactions] Amount match:" << tx.amount
- << (matchedX100 ? "(scaled x100)" : "(direct)")
<< "date:" << tx.date << "time:" << tx.time
<< "→ target idx" << matchIdx;
@@ -947,8 +947,8 @@ void GetLastTransactionsScript::searchAll(
// Формируем task_result напрямую из распарсенных данных и отдаём
// EventHandler через сигнал — без записи/чтения БД.
{
- const double finalAmount = matchedX100 ? detail.amount * 100.0 : detail.amount;
- const double finalFee = matchedX100 ? detail.fee * 100.0 : detail.fee;
+ const double finalAmount = detail.amount;
+ const double finalFee = detail.fee;
QDateTime txTime = QDateTime::fromString(
detail.date + " " + detail.time, "dd.MM.yyyy HH:mm:ss");
diff --git a/monitoring/app.py b/monitoring/app.py
index f3ee481..4825d91 100644
--- a/monitoring/app.py
+++ b/monitoring/app.py
@@ -982,7 +982,7 @@ def admin_logout() -> Response:
return resp
-def _query_entries(kind, type_, event, q, date_from, date_to, page):
+def _query_entries(kind, type_, event, q, date_from, date_to, page, q2=""):
where, params = [], []
if kind:
where.append("kind=?"); params.append(kind)
@@ -992,6 +992,8 @@ def _query_entries(kind, type_, event, q, date_from, date_to, page):
where.append("event LIKE ?"); params.append(f"%{event}%")
if q:
where.append("message LIKE ?"); params.append(f"%{q}%")
+ if q2:
+ where.append("message LIKE ?"); params.append(f"%{q2}%")
df = _parse_date(date_from)
if df is not None:
where.append("ts>=?"); params.append(df)
@@ -1096,6 +1098,7 @@ def admin_dashboard(
type: str = "",
event: str = "",
q: str = "",
+ q2: str = "",
date_from: str = "",
date_to: str = "",
page: int = 0,
@@ -1103,11 +1106,11 @@ def admin_dashboard(
if not _is_authed(request):
return RedirectResponse("/admin/login", status_code=303)
- rows, total, types = _query_entries(kind, type, event, q, date_from, date_to, page)
+ rows, total, types = _query_entries(kind, type, event, q, date_from, date_to, page, q2)
page = max(0, page)
current_url = (f"/admin?kind={kind}&type={html_lib.escape(type)}&event={html_lib.escape(event)}"
- f"&q={html_lib.escape(q)}&date_from={date_from}&date_to={date_to}&page={page}")
+ f"&q={html_lib.escape(q)}&q2={html_lib.escape(q2)}&date_from={date_from}&date_to={date_to}&page={page}")
trs = _build_entry_trs(rows, current_url)
@@ -1116,7 +1119,7 @@ def admin_dashboard(
def link(p: int) -> str:
qs = (f"kind={kind}&type={html_lib.escape(type)}&event={html_lib.escape(event)}"
- f"&q={html_lib.escape(q)}&date_from={date_from}&date_to={date_to}&page={p}")
+ f"&q={html_lib.escape(q)}&q2={html_lib.escape(q2)}&date_from={date_from}&date_to={date_to}&page={p}")
return f"/admin?{qs}"
shown_from = page * PAGE_SIZE + 1 if total else 0
@@ -1133,6 +1136,7 @@ def admin_dashboard(
+
@@ -1219,6 +1223,7 @@ def admin_entries_fragment(
type: str = "",
event: str = "",
q: str = "",
+ q2: str = "",
date_from: str = "",
date_to: str = "",
page: int = 0,
@@ -1227,8 +1232,8 @@ def admin_entries_fragment(
raise HTTPException(status_code=401, detail="auth required")
page = max(0, page)
current_url = (f"/admin?kind={kind}&type={html_lib.escape(type)}&event={html_lib.escape(event)}"
- f"&q={html_lib.escape(q)}&date_from={date_from}&date_to={date_to}&page={page}")
- rows, total, _ = _query_entries(kind, type, event, q, date_from, date_to, page)
+ f"&q={html_lib.escape(q)}&q2={html_lib.escape(q2)}&date_from={date_from}&date_to={date_to}&page={page}")
+ rows, total, _ = _query_entries(kind, type, event, q, date_from, date_to, page, q2)
trs = _build_entry_trs(rows, current_url)
return JSONResponse({"rows_html": "".join(trs), "total": total})
diff --git a/services/EventHandler.cpp b/services/EventHandler.cpp
index c862f9f..4b5f104 100644
--- a/services/EventHandler.cpp
+++ b/services/EventHandler.cpp
@@ -719,6 +719,20 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
const auto thr = new QThread(this);
const QString key = account.deviceId;
+ // Перед запуском любой задачи гарантируем вертикальную ориентацию экрана:
+ // если телефон в landscape — принудительно ставим portrait. Выполняется в
+ // рабочем потоке (functor без context → исполняется в потоке-эмиттере thr),
+ // поэтому adb-I/O не блокирует поток диспетчера. Коннект добавлен до setup(),
+ // значит сработает раньше, чем worker::start → doStart().
+ const QString canonicalDeviceId = account.deviceId;
+ connect(thr, &QThread::started, [canonicalDeviceId]() {
+ DeviceInfo dev = DeviceDAO::getDeviceById(canonicalDeviceId);
+ if (dev.id.isEmpty()) dev = DeviceDAO::findByAdbSerial(canonicalDeviceId);
+ const QString serial = dev.adbSerial.isEmpty() ? canonicalDeviceId
+ : dev.adbSerial;
+ AdbUtils::ensurePortraitOrientation(serial);
+ });
+
// Generic helper: подключает сигналы и переносит worker в поток
auto setup = [&](auto *worker) {
using W = std::remove_pointer_t;