dashboard: seimbangkan kolom dan responsifkan riwayat
This commit is contained in:
1 parent
9a269e224c
commit
e4fa6bf1dc
6 files changed
+34
-10
No files matched your search
@@ -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=<txid>` 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=<BP>`. 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.
|
||||
|
||||
@@ -54,7 +54,7 @@ api: GET `https://api.databisnis.id/v2/history/get_transaction?id=<txid>` (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,6 +116,7 @@ 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
|
||||
|
||||
@@ -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).
|
||||
|
||||
+15
-4
@@ -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);
|
||||
|
||||
@@ -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') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css', v=3) }}">
|
||||
</head>
|
||||
<body>
|
||||
<header class="top-nav">
|
||||
@@ -34,7 +34,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="spec-list">
|
||||
<section class="spec-list run-list">
|
||||
<div class="list-header">
|
||||
<span>RUN</span>
|
||||
<span>TANGGAL</span>
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
<p class="caption">TRANSFER PER PEMILIH</p>
|
||||
|
||||
<section class="spec-list">
|
||||
<section class="spec-list pay-list">
|
||||
<div class="list-header">
|
||||
<span>AKUN</span>
|
||||
<span>TANGGAL</span>
|
||||
|
||||
@@ -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') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css', v=3) }}">
|
||||
</head>
|
||||
<body>
|
||||
<header class="top-nav">
|
||||
|
||||
Reference in new issue
Block a user