Files
databisnisid/AGENTS.md
T
proitlab 656f26085f T18: stack swarm docker — image web+scan, scan_loop.py, mariadb bind-mount server5.saltis.id
- Dockerfile (web) + Dockerfile.scan (scan; sertakan tzdata) + .dockerignore
- scan_loop.py: jadwal skan SCAN_HOURS (0,8,16 lokal via TZ) + skan-awal SCAN_RUN_ON_START; loop tak keluar; tunggu DB siap
- docker-compose.yml → STACK SWARM produksi (mariadb bind mount /data/db/mariadb/databisnisid/data + placement node.hostname == server5.saltis.id; web :5000; scan); image dari registry git.proit.id/proitlab/databisnisid-{web,scan}
- docker-compose.dev.yml: mariadb uji lokal (127.0.0.1:3306) — pengganti docker-compose.yml lama
- SPEC.md §V18/T18; AGENTS.md runbook swarm + dev compose
- .env.example: TZ/SCAN_HOURS/SCAN_RUN_ON_START/MARIADB_*
2026-08-05 14:26:29 +07:00

9.7 KiB
Raw Blame History

AGENTS.md

Two tools scan the Vexanium blockchain voters table for accounts whose only vote goes to this BP (databisnisid): a Node.js reference script and the Python production tool that persists results to SQLite.

Run

  • Node reference script: node get_voters.js (no deps, Node 18+).
  • Python tool (production): ./venv/bin/python get_voters.py. venv is Python 3.12, deps requests + flask + python-dotenv + gunicorn + pymysql (mysql backend only) (requirements.txt). Install with ./venv/bin/pip install -r requirements.txt.
  • Storage backend: db.py abstracts it. Default sqlite (VEX_DB_PATH, stdlib sqlite3, WAL). Optional mysql (VEX_DB_BACKEND=mysql + VEX_DB_HOST/PORT/USER/PASS/NAME, PyMySQL). Oracle tests stay on sqlite; test_mariadb.py is opt-in (skips unless VEX_DB_BACKEND=mysql). Query SQL is written once with %s placeholders (translated to ? for sqlite); db.query always returns a list.
  • Test MariaDB/MySQL via docker: docker compose -f docker-compose.dev.yml up -d (mariadb:11 container databisnisid-mariadb, localhost-only 127.0.0.1:3306, db/user/pass databisnisid/databisnis/databisnis, named volume, healthcheck). Stop/remove with docker compose -f docker-compose.dev.yml down; wipe data with docker compose -f docker-compose.dev.yml down -v. Verify with docker compose -f docker-compose.dev.yml exec mariadb mariadb -u databisnis -pdatabisnis databisnisid -e 'SELECT 1'. Smoke against the container: VEX_DB_BACKEND=mysql VEX_DB_HOST=127.0.0.1 VEX_DB_PORT=3306 VEX_DB_USER=databisnis VEX_DB_PASS=databisnis VEX_DB_NAME=databisnisid ./venv/bin/python test_mariadb.py
  • Docker Swarm (production stack): images are registry-pushed git.proit.id/proitlab/databisnisid-web + databisnisid-scan — build & push them first (docker build -t git.proit.id/proitlab/databisnisid-web . && docker push git.proit.id/proitlab/databisnisid-web, same for the scan image), then from a swarm manager run docker stack deploy -c docker-compose.yml databisnisid. Stack = mariadb (internal) + web (gunicorn dashboard, published :5000) + scan (scan_loop.py, runs get_voters.py at SCAN_HOURS default 0,8,16, local timezone TZ default Asia/Jakarta, plus one scan at container start via SCAN_RUN_ON_START). mariadb is pinned by placement.constraints: node.hostname == server5.saltis.id because its data lives in the host bind mount /data/db/mariadb/databisnisid/data on that node; web/scan can run on any node and reach it over the overlay network appnet. Inspect: docker stack services databisnisid, docker service logs databisnisid_scan, docker stack rm databisnisid. docker stack deploy ignores build: (images must already be in the registry) and ignores env_file (env is inlined with ${VAR} interpolation from .env). The local dev box has no swarm anymore (torn down) — if you re-init one there, the mariadb constraint leaves that task Pending since no node is named server5.saltis.id.
  • Web dashboard (read-only, reads the store via db.py): production ./venv/bin/gunicorn -c gunicorn.conf.py dashboard:app → http://127.0.0.1:5000/ (run from repo dir). Easier: ./run.sh (same command, works from any cwd, $@ passed through). gunicorn.conf.py imports config → .env honored; DASH_WORKERS (default 2) controls workers, DASH_HOST/DASH_PORT the bind. Dev server (single-process) still works via ./venv/bin/python dashboard.py. Paged 50/page, sorted staked DESC, live owner search (/api/search). Data freshness comes from the daily scan run — the dashboard never scans.
  • Config: all tunables load from env / .env via config.py (python-dotenv): VEX_TARGET_BP, VEX_API_NODE, VEX_DB_PATH, VEX_DB_BACKEND, VEX_DB_HOST, VEX_DB_PORT, VEX_DB_USER, VEX_DB_PASS, VEX_DB_NAME, VEX_MIN_STAKED_VEX, DASH_PAGE_SIZE, DASH_HOST, DASH_PORT, DASH_WORKERS, VEX_STALE_DAYS, DATABISNIS_API, DASH_LIQUID_TTL. Copy .env.example → .env to override; .env is gitignored. Chain constants (vexcore/scope/table) stay hardcoded.
  • Syntax check: node --check get_voters.js, ./venv/bin/python -m py_compile config.py get_voters.py dashboard.py db.py gunicorn.conf.py scan_loop.py test_get_voters.py test_dashboard.py test_mariadb.py.
  • Tests (the verification oracles): ./venv/bin/python test_get_voters.py and ./venv/bin/python test_dashboard.py must both exit 0. They mock the network / use a temp DB — the live node is too flaky/slow for a full-scan test. Run after touching the relevant file.

Key facts

  • TARGET_BP (databisnisid) and API_NODE (https://v2.vexascan.com:2096) are the defaults in config.py (env-overridable); the JS reference still hardcodes them at the top of the file.
  • The public Vexanium RPC node is flaky/timeout-prone; both scripts have retry logic built in — don't remove or bypass it. A full scan of the voters table takes minutes.
  • The system contract on Vexanium is vexcore (not vexio), scope is also vexcore. Do not "fix" this to match EOS convention.
  • VEX stake is stored scaled by 10000; the Python tool divides by 10000 before storing.
  • The node sometimes returns staked as a string instead of a number — normalize coerces to float (spec §V8).
  • The voters table has a sentinel first row (owner="...........q", uint64 0) that must be skipped.
  • Authoritative vote weight is last_vote_weight (high-precision decimal string) — stored verbatim as TEXT, never float-converted (spec §V6).
  • Freshness filter (spec §V13): last_vote (estimated last re-vote date) is derived at scan time from the Vexanium weight formula last_vote_weight = staked_raw × 2^(years since 2000) → log2(weight / (staked×10000)). Voters whose last_vote is older than VEX_STALE_DAYS (default 28) — or can't be derived (zero/empty weight) — are dropped in normalize() and never stored; the dashboard therefore shows only fresh voters (no stale UI). The heuristic over-reads when a voter unstaked without re-voting (weight ÷ smaller stake ⇒ future date), so normalize() clamps any last_vote past scan time down to now.
  • 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.
  • 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

  • SPEC.md — spec (goal/constraints/interfaces/invariants/tasks/bug log), in caveman encoding. Build/backprop flow through it.
  • get_voters.py — production fetcher: scan → filter (stake + freshness, basi dibuang) → db.replace_snapshot (each run replaces the table = daily snapshot, with scanned_at + derived last_vote; never appends history).
  • db.py — storage abstraction (sqlite default | mysql via PyMySQL); connect/query/queryone/replace_snapshot; %s → ? for sqlite; query returns list.
  • get_voters.js — reference implementation only.
  • dashboard.py + templates/index.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.
  • 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; 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.
  • Dockerfile — image web databisnisid-web (gunicorn dashboard; DASH_HOST=0.0.0.0 di stack agar ingress menjangkaunya). Dockerfile.scan — image databisnisid-scan (scan_loop; sertakan tzdata). Di stack produksi keduanya di-push ke registry git.proit.id/proitlab/databisnisid-{web,scan} (⊥ build: di compose). .dockerignore — venv/.env/artifak tak masuk build context.
  • docker-compose.yml — STACK SWARM PRODUKSI (mariadb internal + web :5000 + scan); image dari registry git.proit.id/proitlab/databisnisid-*; mariadb bind mount /data/db/mariadb/databisnisid/data + placement node.hostname == server5.saltis.id; network overlay appnet. docker-compose.dev.yml — mariadb uji lokal (127.0.0.1:3306, volume mariadb_data).
  • DESIGN.md — Bugatti austere style guide; the dashboard's CSS maps its tokens (canvas #000000, hairline #262626, weight 400 everywhere, fonts Saira Condensed / EB Garamond / JetBrains Mono).
  • voters.db — SQLite output (daily snapshot, gitignored in spirit).

Conventions

  • Comments and console output are in Indonesian — keep new output/comments in Indonesian.
  • Dashboard UI labels are Indonesian uppercase captions (e.g. "DAFTAR PEMILIH", "TOTAL PEMILIH").
  • JS style: 2-space indent, semicolons, single quotes, trailing commas, async/await.
  • Python style: 4-space indent, stdlib sqlite3, requests, flask, PEP8.
  • New SQL shared across backends: %s placeholders (never ?), ESCAPE '!' for LIKE (backslash breaks MySQL string literals), no MySQL-only syntax.