From a5e83673632aae572f38d101383ca6bdfabd40ff Mon Sep 17 00:00:00 2001 From: dsutanto Date: Sat, 8 Aug 2026 21:40:23 +0700 Subject: [PATCH] =?UTF-8?q?promo=20VEXWALLET=20pakai=20logo=20IDRS=20(stat?= =?UTF-8?q?ic/idrs.png)=20+=20BARU=20band=20kolom=20TOTAL=20REWARD=20?= =?UTF-8?q?=E2=86=92=20MATURITY=20PERIOD=20(V52/V53)=20;=20image=20web=20r?= =?UTF-8?q?ebuild=20+=20push?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 +- SPEC.md | 3 ++- static/idrs.png | Bin 0 -> 17275 bytes static/style.css | 28 ++++++++++++++++++++++++---- templates/index.html | 17 +++++++++++++---- test_dashboard.py | 14 +++++++++++--- 6 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 static/idrs.png diff --git a/AGENTS.md b/AGENTS.md index 21b95b7..579fc70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ Two tools scan the Vexanium blockchain voters table for accounts whose only vote - `claim.py` — klaim reward BP harian: baca `last_claim_time` (tabel `producers`, di-retry V46) → `next_window` = +24 jam, math UTC-naive via `_utcnow()` (V49/B13 — ⊥ `datetime.now()` lokal yang geser +TZ → poll prematur) → (≥24 jam lewat → clamp ke now, recovery B8); tidur sampai mendekat, lalu poll `vexcore::claimrewards` (sign pyntelope `VEX_BP_PRIVATE_KEY`) tiap `CLAIM_RETRY_SECONDS`; deteksi mendarat via `_confirm_landed` di KEDUA jalur (send sukses & exception) = Hyperion `executed` ATAU `last_claim_time` maju dari baseline (V46/B11 — `send()` pyntelope ⊥ raise saat chain menolak, HTTP 500 sbg body; Hyperion `executed: false` utk tx baru-mendarat-belum-terindeks juga dicek lewat `_claim_time_advanced`, V50/B14); tak terkonfirmasi → retry senyap; reward = isi tx via Hyperion (bpay+vpay → BP, `reward_from_tx`) prioritas, selisih saldo fallback; reward ⊥ terukur → fee `pending` + notif `KLAIM MENDARAT · REWARD TAK TERUKUR` (V33/B10); fee 10% (`VEX_BP_FEE_PERCENT`) floor 4-des → `VEX_BP_FEE_WALLET` via `distribute.build_signed_transfer`, memo `BP FEE YYYY-MM-DD`; parse `get_table_rows` pakai `data.get('rows')` (bentuk asli dict, ⊥ `data[0]` — B7/V39); `step()` = state machine (resume fee → jadwal → poll), source of truth `claim_runs` (fee pending/failed diulang + resume saat restart). Modul logika (⊥ CLI). `test_claim.py` — oracle (mock chain+pyntelope, bentuk respons asli, temp DB). - `claim_loop.py` — scheduler harian dalam container stack (service `claim`): `_wait_db` (duplikat scan_loop) lalu `claim.step()` terus, loop tak pernah keluar. - `telegram.py` — kirim notifikasi status distribusi + klaim via Telegram Bot API (best-effort; ⊥ token/chat id → kanal no-op senyap; gagal kirim → log saja). Dua kanal: `TELEGRAM_COMMUNITY_CHAT_IDS` (CSV) = brief mulai/selesai distribusi saja; `TELEGRAM_INTERNAL_CHAT_IDS` (CSV) = detail distribusi + semua kegagalan + klaim reward (⊥ komunitas, V30/V36). `send_text(text, chat_ids=None)` = komunitas, `send_internal(text)` = internal; dari env/`.env`/NFS `config.env`. -- `dashboard.py` + `templates/index.html` + `templates/history.html` + `static/style.css` + `static/app.js` — Flask web dashboard; reads the store via `db.py`, styled per `DESIGN.md`; `app.js` = debounced live owner search (fetch `/api/search`), degrades to the server-side `?q=` GET form if JS is off. Three vote-age bands (V40/V52): the voters list is split into BARU (top, `PEMILIH BARU · BELUM MATANG`, new accounts by `first_seen_at` within 3 days) and the main list (`voter-list` section, `.reward` cells, V29) where KADALUARSA rows are pinned at the top with amber styling + `VOTE ULANG` badge + legend before the paged VALID rows, which LEFT-joins a `status='sent'` payments aggregate for the TOTAL REWARD column; since V42 the VALID list also holds REVOTE (returning re-voters, `voter_first_seen.first_seen_at ≤ now−3d`) tagged with a mint `REVOTE` tag (legend `TAG MINT = REVOTE KURANG DARI 1 HARI · VALID LANGSUNG`, shows only while `last_vote > now−VEX_REVOTE_TAG_DAYS`) — no separate band, no maturity wait; only genuinely new voters sit in BARU tagged `BARU`; since V43 VALID rows within `VEX_WARN_DAYS` of the KADALUARSA cutoff get an amber dashed `VOTE ULANG SEGERA` badge + legend `TAG AMBER = KADALUARSA DALAM 3 HARI · N PEMILIH` (display-only); search (`?q=` and `/api/search`) covers all bands and tags each result (`group` = `baru|revote|valid|kadaluarsa` + `expired` + `expiring`). Banner BERITA (V38) shows the next distribution run from `DISTRIBUTE_HOUR`/`DISTRIBUTE_WEEKDAY`/`DISTRIBUTE_FIRST_RUN` + `TZ` (mirror of `distribute_loop.next_boundary`, ⊥ impor lintas-image; drift guarded in test_dashboard); the `SETIAP ... · HH:00` recurrence line renders only for regular weekly rounds — hidden while the next event is the one-off first-run (`_is_first_run_next`, date ≠ weekly day). A `stats-note` caption states the list is single-vote-to-BP accounts ≥ min stake (V47); account names link to `https://vexascan.com/account/{owner}` in the index list, `/history` pay-list, and live search (`.owner-link`, V51). `/history` = riwayat distribusi read-only; `run-list` = six-column desktop/tablet grid, `pay-list` = four-column grid (AKUN/JUMLAH/STATUS/TXID) showing only the latest run with its date in the title, and labeled mobile cards. +- `dashboard.py` + `templates/index.html` + `templates/history.html` + `static/style.css` + `static/app.js` — Flask web dashboard; reads the store via `db.py`, styled per `DESIGN.md`; `app.js` = debounced live owner search (fetch `/api/search`), degrades to the server-side `?q=` GET form if JS is off. Three vote-age bands (V40/V52): the voters list is split into BARU (top, `PEMILIH BARU · BELUM MATANG`, new accounts by `first_seen_at` within 3 days) and the main list (`voter-list` section, `.reward` cells, V29) where KADALUARSA rows are pinned at the top with amber styling + `VOTE ULANG` badge + legend before the paged VALID rows, which LEFT-joins a `status='sent'` payments aggregate for the TOTAL REWARD column; since V42 the VALID list also holds REVOTE (returning re-voters, `voter_first_seen.first_seen_at ≤ now−3d`) tagged with a mint `REVOTE` tag (legend `TAG MINT = REVOTE KURANG DARI 1 HARI · VALID LANGSUNG`, shows only while `last_vote > now−VEX_REVOTE_TAG_DAYS`) — no separate band, no maturity wait; only genuinely new voters sit in BARU tagged `BARU`; since V43 VALID rows within `VEX_WARN_DAYS` of the KADALUARSA cutoff get an amber dashed `VOTE ULANG SEGERA` badge + legend `TAG AMBER = KADALUARSA DALAM 3 HARI · N PEMILIH` (display-only); search (`?q=` and `/api/search`) covers all bands and tags each result (`group` = `baru|revote|valid|kadaluarsa` + `expired` + `expiring`). Banner BERITA (V38) shows the next distribution run from `DISTRIBUTE_HOUR`/`DISTRIBUTE_WEEKDAY`/`DISTRIBUTE_FIRST_RUN` + `TZ` (mirror of `distribute_loop.next_boundary`, ⊥ impor lintas-image; drift guarded in test_dashboard); the `SETIAP ... · HH:00` recurrence line renders only for regular weekly rounds — hidden while the next event is the one-off first-run (`_is_first_run_next`, date ≠ weekly day). A `stats-note` caption states the list is single-vote-to-BP accounts ≥ min stake (V47); account names link to `https://vexascan.com/account/{owner}` in the index list, `/history` pay-list, and live search (`.owner-link`, V51); a promo card (`BY DATABISNISID · VEXWALLET`) links the VexWallet Android app on the index page via the IDRS logo (`static/idrs.png`) + `UNDUH DI PLAY STORE` (V53); BARU rows show a `MATURITY PERIOD` countdown column (`VALID DALAM N HARI`) instead of TOTAL REWARD. `/history` = riwayat distribusi read-only; `run-list` = six-column desktop/tablet grid, `pay-list` = four-column grid (AKUN/JUMLAH/STATUS/TXID) showing only the latest run with its date in the title, and labeled mobile cards. - `gunicorn.conf.py` — gunicorn production config (bind/workers from `config`, sync worker). `run.sh` — launcher: `./run.sh` = `./venv/bin/gunicorn -c gunicorn.conf.py dashboard:app` from any cwd. - `config.py` — loads env/`.env` (python-dotenv) → `TARGET_BP`, `API_NODE`, `DB_PATH`, `DB_BACKEND`, `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASS`, `DB_NAME`, `MIN_STAKED_VEX`, `PAGE_SIZE`, `DASH_HOST`, `DASH_PORT`, `DASH_WORKERS`, `VEX_STALE_DAYS`, `VEX_MATURITY_DAYS`, `VEX_EXPIRED_DAYS`, `DATABISNIS_API`, `DASH_LIQUID_TTL`, `BP_PRIVATE_KEY`, `DISTRIBUTE_HOUR`, `DISTRIBUTE_MAX_ATTEMPTS`, `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_IDS`, `CLAIM_RETRY_SECONDS`, `BP_FEE_WALLET`, `BP_FEE_PERCENT`; shared by get_voters & dashboard. - `scan_loop.py` — scheduler dalam container stack: menunggu batas `SCAN_HOURS` (lokal via `TZ`), panggil `get_voters.main()`; skan-awal `SCAN_RUN_ON_START` + tunggu DB siap; loop tak pernah keluar. diff --git a/SPEC.md b/SPEC.md index 2746375..83c0411 100644 --- a/SPEC.md +++ b/SPEC.md @@ -117,7 +117,8 @@ V45: anti false-success klaim: klaim dianggap "mendarat" ⊥ cukup karena `last_ V46: ⊥ pernah percaya `signed.send()` semata utk deteksi mendarat — pyntelope TIDAK raise saat chain menolak (`Net._request` mengembalikan body HTTP 500 sbg JSON, ⊥ exception), jadi "sukses kirim" bisa berarti DITOLAK (mis. claimrewards sebelum jendela 24 jam) → false success reward 0. Landing dikonfirmasi di KEDUA jalur (send sukses maupun exception) lewat `_confirm_landed` = Hyperion `executed` ATAU `last_claim_time` maju dari baseline; tak terkonfirmasi → `retry` senyap (⊥ catat `claim_runs`, ⊥ notif sukses) — klaim sah yang belum terindeks Hyperion → retry 60s berikutnya terverifikasi (self-healing dalam 2 jam poll). `next_window`: semua pembacaan producer gagal → raise (loop catch-and-sleep), ⊥ clamp now prematur; `now` hanya utk pembacaan sukses tanpa klaim sebelumnya (BP baru). Oracle test_claim: send-normal+verify-False+last-maju → claimed (V50); send-normal+verify-False+last-tak-maju → retry + ⊥ baris + ⊥ notif; baca-producer-gagal → raise V49: math jendela klaim memakai `_utcnow()` (UTC-naive, `datetime.now(timezone.utc).replace(tzinfo=None)`) — konsisten dgn `last_claim_time` chain (UTC) & `get_voters`/`dashboard`; `step()`/`poll_claim`/`_try_claim_once` memakai `_utcnow()` (⊥ `datetime.now()` lokal yang bergeser +TZ di container → jendela maju ~7 jam → poll prematur → ditolak chain → retry tanpa henti, B13); oracle test_claim (`_utcnow` tzinfo None; step before window → sleep) V50: `_confirm_landed` ⊥ langsung menyerah saat Hyperion `executed: false` (verify_txid → False): tx yang BARU SAJA mendarat bisa belum terindeks Hyperion (lag, response `executed: false` utk id yg belum diindeks) — menganggap False sbg "tak mendarat" membuat klaim sukses masuk retry-loop permanen (B14). Landing = Hyperion `executed` ATAU `last_claim_time` maju dari baseline; utk verified False MAUPUN None tetap cek `_claim_time_advanced`; hanya bila itu juga False → retry (klaim ditolak ⊥ memajukan `last_claim_time`). Oracle test_claim: send-normal+verify-False+last-maju → claimed + reward tercatat (V50); send-normal+verify-False+last-tak-maju → retry + ⊥ baris (V46) -V52: maturity pemilih baru diukur dari `first_seen_at` (stempel scan presisi detik, tabel `voter_first_seen`) BUKAN `last_vote` — `last_vote` terkuantisasi PEKAN oleh stake2vote (B12) sehingga voter baru yang vote Senin–Kamis tampak 4–6 hari ⇒ lolos maturity 3-hari tanpa menunggu (B15). Aturan (dashboard + distribusi): BARU = `last_vote > now−STALE` DAN (`first_seen_at` NULL atau `> now−MATURITY`); VALID = `last_vote > now−STALE` DAN `first_seen_at ≤ now−MATURITY` (akun matang, ⊖ perlu vote tua). `_edge_rows`/`_db_rows`/`_count`/`_stats`/`_search_rows` + distribusi memakai ambang `first_seen_at`; `_stats` partisi `n_new + n_valid + n_expired == total_voters`; oracle test_distribute (make_voters_db seed first_seen matang default, make_first_seen REPLACE utk BARU/REVOTE) + test_dashboard (acct### seed first_seen matang) | V52,V40,V42,V41 +V52: maturity pemilih baru diukur dari `first_seen_at` (stempel scan presisi detik, tabel `voter_first_seen`) BUKAN `last_vote` — `last_vote` terkuantisasi PEKAN oleh stake2vote (B12) sehingga voter baru yang vote Senin–Kamis tampak 4–6 hari ⇒ lolos maturity 3-hari tanpa menunggu (B15). Aturan (dashboard + distribusi): BARU = `last_vote > now−STALE` DAN (`first_seen_at` NULL atau `> now−MATURITY`); VALID = `last_vote > now−STALE` DAN `first_seen_at ≤ now−MATURITY` (akun matang, ⊖ perlu vote tua). `_edge_rows`/`_db_rows`/`_count`/`_stats`/`_search_rows` + distribusi memakai ambang `first_seen_at`; `_stats` partisi `n_new + n_valid + n_expired == total_voters`; BARU band kolom MATURITY PERIOD = countdown `VALID DALAM N HARI` (`first_seen + MATURITY`, filter `countdown`; ⊖ TOTAL REWARD yang selalu 0 utk pemilih baru); oracle test_distribute (make_voters_db seed first_seen matang default, make_first_seen REPLACE utk BARU/REVOTE) + test_dashboard (acct### seed first_seen matang; band check MATURITY PERIOD) | V52,V40,V42,V41 +V53: kartu promo VEXWALLET di index (display-only): section `.news` tag `BY DATABISNISID`, judul `VEXWALLET · ANDROID`, link logo `static/idrs.png` (`.promo-logo`) + label `UNDUH DI PLAY STORE` → `https://play.google.com/store/apps/details?id=kriptoteknologi.io.vexwallet&hl=id` (`target=_blank rel=noopener`); oracle test_dashboard (logo idrs.png + link + tag + label ada di `/`); history ⊖ berubah V35: kill-switch distribusi `DISTRIBUTE_ENABLED` default false → run nyata (bukan dry-run) no-op: exit 0, ⊥ baca saldo, ⊥ tanda tangan, ⊥ tulis DB, ⊥ notif; `--dry-run` tetap menampilkan rencana (read-only); nilai `true`/`1`/`yes` → normal V36: notifikasi klaim reward (sukses `notify_claim` & gagal `notify_claim_failure`) → kanal INTERNAL saja, ⊥ pernah ke KOMUNITAS (V30); fee gagal ⊥ spam — satu notif per klaim (saat transisi ke `failed`) V37: jadwal distribusi MINGGUAN (⊥ harian): `distribute_loop` menunggu `DISTRIBUTE_WEEKDAY` — SATU hari (`sat`) atau CSV beberapa hari (`wed,sat`), nama `mon`..`sun` atau 0-6 (V44: multi-hari, event terdekat di antara hari-hari terdaftar) — jam `DISTRIBUTE_HOUR` (lokal TZ); plus satu one-off `DISTRIBUTE_FIRST_RUN` (`YYYY-MM-DD`; default baked `2026-08-17`) yang diambil bila lebih dekat dari mingguan; setelah lewat (atau ⊥ diset) → hanya mingguan; batas dihitung ulang tiap iterasi loop (setelah run → jadwal berikutnya); payout di-tunda bila `DISTRIBUTE_ENABLED=false` (V35) — jadwal tetap maju, run jadi no-op; `distribute_loop.main` baca jadwal via `config` (⊥ `os.getenv` sendiri) agar konsisten dgn dashboard V38 diff --git a/static/idrs.png b/static/idrs.png new file mode 100644 index 0000000000000000000000000000000000000000..3ab91edbf57db0b660255e734b217a5b5b230352 GIT binary patch literal 17275 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 Ux$BR182|tP07*qoM6N<$g4p$D-v9sr literal 0 HcmV?d00001 diff --git a/static/style.css b/static/style.css index 52d4e4b..bde31ca 100644 --- a/static/style.css +++ b/static/style.css @@ -277,14 +277,34 @@ button.pill { text-underline-offset: 3px; } .owner-link:hover { color: var(--link); } -.valid-in { - display: block; - margin-top: var(--sp-xxs); +.maturity { font-family: var(--font-mono); - font-size: 9px; + font-size: 11px; letter-spacing: 1.5px; + text-align: center; color: var(--muted); } +.promo { + display: inline-flex; + align-items: center; + gap: var(--sp-md); + text-decoration: none; + color: var(--on-dark); +} +.promo-logo { + width: 56px; + height: 56px; + border-radius: 12px; + border: 1px solid var(--hairline-strong); +} +.promo-cta { + font-family: var(--font-mono); + font-size: 14px; + letter-spacing: 2.5px; + text-transform: uppercase; + color: var(--on-dark); +} +.promo:hover .promo-cta { color: var(--link); } /* ————— band BARU / KADALUARSA (V40) ————— */ .band-list { margin-top: var(--sp-xl); } diff --git a/templates/index.html b/templates/index.html index 3cf171c..e6df40c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,7 +7,7 @@ - +
@@ -32,6 +32,15 @@ {% endif %} +
+ BY DATABISNISID +

VEXWALLET · ANDROID

+ + + UNDUH DI PLAY STORE + +
+
{{ liquid_vex|vex if liquid_vex is not none else '—' }} @@ -95,15 +104,15 @@ RANK AKUN STAKE (VEX) - TOTAL REWARD (VEX) + MATURITY PERIOD
{% for row in new_rows %}
{{ loop.index }} - {{ row[0] }} BARU{% if row[4] %}{{ row[4]|countdown }}{% endif %} + {{ row[0] }} BARU {{ row[1]|vex }} - {{ row[3]|vex }} + {{ row[4]|countdown }}
{% endfor %}
diff --git a/test_dashboard.py b/test_dashboard.py index fb0e7f3..8b395c0 100644 --- a/test_dashboard.py +++ b/test_dashboard.py @@ -128,6 +128,11 @@ def main(): check('V47 catatan definisi total vs vexascan', 'SUARA TUNGGAL KE' in html and 'BUKAN TOTAL VOTE-WEIGHT' not in html) + check('V53 promo VEXWALLET link + logo IDRS', + 'play.google.com/store/apps/details?id=kriptoteknologi.io.vexwallet' in html + and 'BY DATABISNISID' in html + and 'UNDUH DI PLAY STORE' in html + and 'idrs.png' in html) check('V40 TOTAL PEMILIH = baris tampil (129)', '129' in html) @@ -147,9 +152,12 @@ def main(): 'PEMILIH REVOTE · BELUM MATANG' not in html) check('V41 caption BARU count', 'PEMILIH BARU · BELUM MATANG (0-3 HARI) · 1' in html) check('V52 BARU countdown VALID DALAM N HARI', - 'valid-in">VALID DALAM 2 HARI' in html) - check('V52 BARU band ⊖ kolom VOTE TERAKHIR', - 'VOTE TERAKHIR' not in _slice_band(html)) + 'maturity" data-label="MATURITY PERIOD">VALID DALAM 2 HARI' in html) + check('V52 BARU band kolom MATURITY PERIOD', + 'MATURITY PERIOD' in _slice_band(html)) + check('V52 BARU band ⊖ kolom VOTE TERAKHIR & TOTAL REWARD', + 'VOTE TERAKHIR' not in _slice_band(html) + and 'TOTAL REWARD' not in _slice_band(html)) check('V42 revoter1 (REVOTE) ada di daftar utama (#rows)', 'revoter1' in _slice_rows(html)) check('V42 newacct1 di BARU, revoter1 di VALID',