promo VEXWALLET pakai logo IDRS (static/idrs.png) + BARU band kolom TOTAL REWARD → MATURITY PERIOD (V52/V53) ; image web rebuild + push

This commit is contained in:
proitlab committed 2026-08-08 21:40:23 +07:00
1 parent cd1452a353
commit a5e8367363
6 files changed
+51 -13

No files matched your search

+1 -1
View File
@@ -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.
+2 -1
View File
@@ -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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+24 -4
View File
@@ -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); }
+13 -4
View File
@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Saira+Condensed:wght@400&family=EB+Garamond:ital,wght@0,400;1,400&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css', v=18) }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css', v=20) }}">
</head>
<body>
<header class="top-nav">
@@ -32,6 +32,15 @@
{% endif %}
</section>
<section class="news">
<span class="news-tag">BY DATABISNISID</span>
<h2 class="news-title">VEXWALLET · ANDROID</h2>
<a class="promo" href="https://play.google.com/store/apps/details?id=kriptoteknologi.io.vexwallet&hl=id" target="_blank" rel="noopener">
<img class="promo-logo" src="{{ url_for('static', filename='idrs.png') }}" alt="IDRS">
<span class="promo-cta">UNDUH DI PLAY STORE</span>
</a>
</section>
<section class="stats">
<div class="spec-cell">
<span class="cell-value">{{ liquid_vex|vex if liquid_vex is not none else '—' }}</span>
@@ -95,15 +104,15 @@
<span>RANK</span>
<span>AKUN</span>
<span>STAKE (VEX)</span>
<span>TOTAL REWARD (VEX)</span>
<span>MATURITY PERIOD</span>
</div>
<div class="list-rows">
{% for row in new_rows %}
<div class="list-row">
<span class="rank">{{ loop.index }}</span>
<span class="owner"><a class="owner-link" href="https://vexascan.com/account/{{ row[0] }}" target="_blank" rel="noopener">{{ row[0] }}</a> <span class="group-tag tag-baru">BARU</span>{% if row[4] %}<span class="valid-in">{{ row[4]|countdown }}</span>{% endif %}</span>
<span class="owner"><a class="owner-link" href="https://vexascan.com/account/{{ row[0] }}" target="_blank" rel="noopener">{{ row[0] }}</a> <span class="group-tag tag-baru">BARU</span></span>
<span class="stake" data-label="STAKE (VEX)">{{ row[1]|vex }}</span>
<span class="reward" data-label="TOTAL REWARD (VEX)">{{ row[3]|vex }}</span>
<span class="maturity" data-label="MATURITY PERIOD">{{ row[4]|countdown }}</span>
</div>
{% endfor %}
</div>
+11 -3
View File
@@ -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)',
'<span class="cell-value">129</span>' 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',