From e4fa6bf1dcbcc74bb12959d1cb1054f41b7c6eb0 Mon Sep 17 00:00:00 2001 From: dsutanto Date: Wed, 5 Aug 2026 17:28:38 +0700 Subject: [PATCH] dashboard: seimbangkan kolom dan responsifkan riwayat --- AGENTS.md | 3 ++- SPEC.md | 6 ++++-- dashboard.py | 10 ++++++++++ static/style.css | 19 +++++++++++++++---- templates/history.html | 6 +++--- templates/index.html | 2 +- 6 files changed, 35 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b6b6d82..42e8a3b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,7 @@ Two tools scan the Vexanium blockchain voters table for accounts whose only vote - The web list shows RANK / AKUN / STAKE (VEX) / VOTE TERAKHIR columns (3 stats cells). Vote weight stays in the DB and `/api/search` JSON but is not rendered as a column. - Token contract is `vex.token` (NOT `eosio.token` — that name doesn't exist on Vexanium), 4-decimal VEX, chain_id `f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f`. Distribution signs `vex.token::transfer` with the BP `active` key. - Distribution (spec §V19–V27): each run pays the whole liquid balance pro-rata by stored `staked`, shares floored at 4 decimals with dust left in the account, one transfer per voter. A txid that can't be confirmed (`GET {DATABISNIS_API}/v2/history/get_transaction?id=` returns no `executed`) is never re-sent the same run — that payment stays `failed` and rejoins the next day. No-op (exit 0, no writes) when the voters table is empty or balance < 0.0001. The dashboard's `/history` view renders `distribute_runs` + `distribute_payments` read-only. +- Dashboard layout (spec §V28): desktop/tablet balances the STAKE column near page center; `/history` uses six-column run/payment grids; mobile turns history rows into labeled cards, including MEMO and TXID. HTML responses use `Cache-Control: no-store`; stylesheet URL is versioned with `?v=` for deploy cache-busting. - Liquid balance (spec §V16): a 4th stat cell "SALDO LIQUID" shows the BP account's liquid VEX (`account.core_liquid_balance`) fetched from `GET {DATABISNIS_API}/v2/state/get_account?account=`. The dashboard fetches it live but caches per-worker in memory for `DASH_LIQUID_TTL` (default 60s); a failed fetch keeps the last value (or renders `—` if none ever succeeded) and the failure is also cooled-down so the API isn't hammered. The dashboard still never writes to the DB. ## Layout @@ -38,7 +39,7 @@ Two tools scan the Vexanium blockchain voters table for accounts whose only vote - `db.py` — storage abstraction (sqlite default | mysql via PyMySQL); `connect/query/queryone/replace_snapshot`; `%s` → `?` for sqlite; `query` returns list. Juga tabel distribusi: `distribute_runs` + `distribute_payments` (append-only) + helper `ensure_distribute_schema/record_run/record_payment/update_payment_status/update_run_status/list_runs/list_payments`. - `get_voters.js` — reference implementation only. - `distribute.py` — pembayaran harian: fetch liquid balance → `compute_shares` (Decimal floor 4-des, sisa di akun) → sign `vex.token::transfer` via pyntelope (`trx.link` ambil ABI+TAPOS dari node, `sign` dengan `VEX_BP_PRIVATE_KEY`, `.send()`) → catat `sent/failed` per voter; verifikasi txid via Hyperion sebelum kirim ulang; `--dry-run` = rencana ⊥ tanda tangan. `test_distribute.py` — oracle (mock chain+pyntelope, temp DB). -- `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. `/history` = riwayat distribusi read-only. +- `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. `/history` = riwayat distribusi read-only; `run-list`/`pay-list` use six-column desktop/tablet grids 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`, `DATABISNIS_API`, `DASH_LIQUID_TTL`, `BP_PRIVATE_KEY`, `DISTRIBUTE_HOUR`, `DISTRIBUTE_MAX_ATTEMPTS`; 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 9fdee9d..03eab36 100644 --- a/SPEC.md +++ b/SPEC.md @@ -54,7 +54,7 @@ api: GET `https://api.databisnis.id/v2/history/get_transaction?id=` (Hyper db: table `distribute_runs` (run_id PK, run_date, balance_start, total_voters, total_staked, total_sent, status, created_at) via `db.py`; append db: table `distribute_payments` (payment_id PK, run_id FK, owner, amount, memo, txid, status[pending|sent|failed], error, created_at, updated_at) via `db.py`; append cmd: `./venv/bin/python distribute.py` → stdout id-ID (rencana + hasil); `--dry-run` → rencana tanpa sign -web: GET `/history` → HTML runs summary + per-voter log, read-only, gaya DESIGN.md +web: GET `/history` → HTML runs summary + per-voter log, read-only, gaya DESIGN.md; 6-kolom desktop/tablet, kartu berlabel mobile env: `VEX_BP_PRIVATE_KEY` (secret swarm / env dev), `DISTRIBUTE_HOUR` (default 10), `DISTRIBUTE_MAX_ATTEMPTS` — via `config.py` (`.env`) ## §V — Invariants @@ -84,6 +84,7 @@ V24: run harian `DISTRIBUTE_HOUR` (default 10) waktu lokal `TZ`; ⊥ beku/carryo V25: pemilih kosong | saldo < 1 unit shareable → no-op senyap, ⊥ tulis DB V26: `VEX_BP_PRIVATE_KEY` ⊥ pernah di-log/print; `--dry-run` utk uji lokal (⊥ sign) V27: dashboard read-only; `/history` baca `distribute_*` via `db.py`; ⊥ mutasi +V28: web list → kolom STAKE/JUMLAH text center; STAKE dekat pusat halaman via grid seimbang; history runs/payments 6 kolom desktop/tablet; mobile → kartu berlabel ∀ field termasuk MEMO/TXID; HTML `no-store` + stylesheet versioned ## §T — Tasks id|status|task|cites @@ -115,9 +116,10 @@ T25|.|scheduler: service `distribute` di stack swarm (Dockerfile.distribute, doc T26|x|dashboard: route `/history` (runs + per-voter log) + template, gaya DESIGN.md, read-only|V27,I.web T27|x|tests: test_distribute.py (mock chain+pyntelope, temp DB: math share, memo, no-op, status/retry, guard duplikat) + extend test_dashboard.py (`/history`)|V20,V21,V22,V25,V26,V27,I.db T28|x|hygiene: .gitignore += `.oc/`; stage penghapusan README.md (stub kosong)| +T29|x|dashboard layout: grid STAKE seimbang desktop/tablet; `/history` 6-kolom + kartu mobile berlabel; CSS cache-bust + HTML no-store|V28,I.web ## §B — Bug log id|date|cause|fix B1|2026-08-04|node balikin `staked` sbg string utk sebagian baris → TypeError int÷int|V8 B2|2026-08-05|`ESCAPE '\'` valid di SQLite tapi memutus literal string MySQL (`'\'` = string tak tertutup) → error 1064 di backend mysql|ganti karakter escape ke `!` (netral di kedua backend), `_escape_like` esc `!`,`%`,`_` (V17) -B3|2026-08-05|PyMySQL `fetchall()` → tuple, SQLite → list → asersi `== []` gagal di mysql|`db.query` bungkus `list()` → tipe seragam (V17) \ No newline at end of file +B3|2026-08-05|PyMySQL `fetchall()` → tuple, SQLite → list → asersi `== []` gagal di mysql|`db.query` bungkus `list()` → tipe seragam (V17) diff --git a/dashboard.py b/dashboard.py index 4673c9e..22a48ca 100644 --- a/dashboard.py +++ b/dashboard.py @@ -21,6 +21,16 @@ MAX_Q_LEN = 64 app = Flask(__name__) + +@app.after_request +def _no_cache_html(resp): + """HTML ⊥ di-cache browser (V28): tautan style.css memakai ?v= utk + memaksa fetch versi baru; halaman itu sendiri tak boleh heuristik cache + sehingga tiap muat selalu menarik tautan ber-version tsb.""" + if resp.mimetype == 'text/html': + resp.headers['Cache-Control'] = 'no-store' + return resp + # Cache saldo liquid per worker (in-memory, ⊥ tulis DB). `at` mencatat # kapan sukses ATAU gagal terakhir → API dihubungi paling banyak sekali # per TTL (cooldown saat gagal pun ikut mencegah pukulan berulang). diff --git a/static/style.css b/static/style.css index e3a7cd1..054d7e7 100644 --- a/static/style.css +++ b/static/style.css @@ -181,12 +181,20 @@ button.pill { .list-header, .list-row { display: grid; - grid-template-columns: 72px 1fr 200px 170px; + grid-template-columns: 72px 1fr 200px 1fr; gap: var(--sp-lg); align-items: baseline; padding: var(--sp-lg) var(--sp-sm); border-bottom: 1px solid var(--hairline); } +.run-list .list-header, +.run-list .list-row { + grid-template-columns: 64px 150px 180px 110px 180px 1fr; +} +.pay-list .list-header, +.pay-list .list-row { + grid-template-columns: 1fr 140px 170px 100px 1.2fr 260px; +} .list-header { font-family: var(--font-mono); font-size: 11px; @@ -284,7 +292,7 @@ button.pill { /* ————— tablet (768–1024px) ————— */ @media (max-width: 1024px) { .list-header, - .list-row { grid-template-columns: 56px 1fr 160px 150px; gap: var(--sp-md); } + .list-row { grid-template-columns: 56px 1fr 160px 1fr; gap: var(--sp-md); } .spec-list, .stats, .search, .search-result { margin-left: var(--sp-md); margin-right: var(--sp-md); } .stats { grid-template-columns: repeat(2, 1fr); } .spec-cell:nth-child(3) { border-left: none; } @@ -308,11 +316,14 @@ button.pill { .list-row .rank { grid-row: 1 / span 3; grid-column: 1; align-self: start; padding-top: 6px; } .list-row .owner { grid-column: 2; font-size: 18px; } .list-row .stake, - .list-row .vote-date { grid-column: 2; } + .list-row .vote-date, + .list-row .txid { grid-column: 2; } .list-row .stake { text-align: left; } + .list-row .txid { font-size: 12px; } .list-row .stake { overflow-wrap: anywhere; } .list-row .stake::before, - .list-row .vote-date::before { + .list-row .vote-date::before, + .list-row .txid::before { content: attr(data-label); display: block; font-family: var(--font-mono); diff --git a/templates/history.html b/templates/history.html index bdb78b5..55d6a63 100644 --- a/templates/history.html +++ b/templates/history.html @@ -7,7 +7,7 @@ - +
@@ -34,7 +34,7 @@ -
+
RUN TANGGAL @@ -61,7 +61,7 @@

TRANSFER PER PEMILIH

-
+
AKUN TANGGAL diff --git a/templates/index.html b/templates/index.html index 82392aa..3589031 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,7 +7,7 @@ - +