Files

43 lines
5.1 KiB
Markdown

# AGENTS.md — IDRS API server
Project conventions and operational gotchas for agents working in this repo.
## Layout
- `app/main.py` — FastAPI app: public endpoints `GET /api/getTokenInfo`, `GET /api/version/android`, `GET /api/getDapps`, `POST /api/increaseDappCounter` (header `X-Api-Key` = env `IDRS_SECRET`, fail-closed 401 if unset/wrong), `GET /img/token/<filename>`, `GET /img/dapps/<filename>` + `/public/img/...` aliases (seed iconUrl path); mounts `/static`; startup calls `db.init_db()`.
- `app/admin.py` — sqladmin panel: `TokenAdmin` (custom WTForms form, `on_model_change`), `VersionAdmin`, `DappAdmin` (URL field + `imageFile` upload, auto `created_at`/`updated_at`, `id` not writable), `WalletAdmin` (unique account, dup rejected, auto `created_at`, `id` not writable), `create_admin(app)`. Upload icons saved under `app/static/icons/token/` with `iconUrl = {IDRS_PUBLIC_URL}/img/token/<filename>`; dapp uploads under `icons/dapps/`.
- `app/models.py` — SQLAlchemy `Token`/`Version`/`Dapp`/`Wallet`, engine, `SessionLocal`, `ICON_DIR`, `DB_PATH` (env `IDRS_DB_PATH`).
- `app/db.py` — seed from `tokenList.json`/`versionAndroid.json`/`dapps.json` (once, empty DB only). Dapp seed mirrors each `image` into `icons/dapps/` and rewrites to `{IDRS_PUBLIC_URL}/img/dapps/<file>` (gate: `image/*` only, sniff `octet-stream`, `text/html`→skip; on failure keep original URL; collision → `-N` suffix). Also seeds first two whitelisted wallets (`susukudaliar`, `vexessential`) when the wallets table is empty.
- `app/verify.py` — icon/link-reachability verifiers: daemon threads started on app startup, gated by env `IDRS_VERIFY_TOKENS` (token `iconUrl`, absent/0/false/off → disabled, default) and `IDRS_VERIFY_DAPPS` (dapp `link`, **enabled by default**; `0/false/off` disables). When enabled, `is_live` = reachability (local file stat under `icons/token/`, or external HTTP HEAD/GET 2xx; dapp link strict 2xx, whitespace-stripped); empty → false; flips after 2 consecutive failed passes, auto-recovers; interval env `IDRS_VERIFY_INTERVAL` (default 600s). When disabled, `is_live` is admin-owned (editable form field). `IDRS_VERIFY_TOKENS` only. Dapp admin-set `is_live=false` is sticky via `is_live_override` (verifier skips overridden dapps). External hosts must be reachable from the server or those tokens/dapps flip false while enabled.
- `app/auth.py` — `AdminAuthBackend`, fail-closed creds.
- `tokenList.json` / `versionAndroid.json` / `dapps.json` — seed source (read-only after seed).
## Environment / secrets
- `.env` (gitignored) → `ADMIN_USERNAME`, `ADMIN_PASSWORD`, `IDRS_PUBLIC_URL` (default `https://idrs.databisnis.id`), `IDRS_SECRET` (dapp counter endpoint; unset → always 401). Test creds `admin`/`testpass123` — change before exposure.
- `.session_secret` (gitignored, auto-generated 0600) if `IDRS_SESSION_SECRET` unset.
- NEVER bake `.env` into the Docker image; pass via `--env-file` / `-e`.
- Untracked, never commit: `.env`, `.session_secret`, `idrs.db*`, `venv/`, `app/static/icons/*`.
## Run (local)
- `./run.sh` loads `.env` then `uvicorn app.main:app --host 127.0.0.1 --port 8000`.
- Start detached: `setsid nohup ./run.sh < /dev/null > /tmp/opencode/idrs.log 2>&1 & disown` (plain `&` hangs).
- Sanity-import: `./venv/bin/python -c "from app import main; print('IMPORT_OK')"`.
## Docker
- `docker build -t idrs-api .` → `docker tag idrs-api:latest git.proit.id/proitlab/idrs-api:latest` → `docker push git.proit.id/proitlab/idrs-api:latest` (registry `git.proit.id`, project `proitlab`).
- Deploy (Swarm): `docker stack deploy -c docker-compose.yml idrs` — service `idrs-api`, traefik labels (Host `idrs.databisnis.id`, `entrypoints=websecure`, `certresolver=letsencrypt`), constraint `node.hostname != server2U`, external overlay `traefik-net` (must pre-exist on manager), `env_file: .env`.
- NFS bind mounts: `/mnt/nfs/server5.saltis.id/data/idrs/data` → `/app/data` (DB), `/mnt/nfs/server5.saltis.id/data/idrs/static` → `/app/app/static` (icons live at `<static>/icons/`; `icons/` subdir auto-created on first upload).
- MUST use a single uvicorn worker (in-memory starlette sessions → `--workers >1` breaks login).
- Behind traefik, uvicorn must trust the proxy scheme or admin assets come out `http://` and the panel is unstyled (mixed content): stack env sets `FORWARDED_ALLOW_IPS=*`.
- DB + uploaded icons persist in the volumes across container recreate; code changes need an image rebuild + re-push.
## Verification
- curl login: `POST /admin/login` with `-c/-b` cookie jar; then curl admin endpoints / uploads with the jar.
- Assert `/img/token/<f>` returns `image/png`; HTML/oversize uploads rejected (400).
## Operation gotchas
- NEVER `pkill -f uvicorn` broadly — a separate user process runs uvicorn on port 7000. Kill only the port-8000 PID (`ps -ef | grep "uvicorn app.main:app --host 127.0.0.1 --port 8000"`).
- Local server binds 127.0.0.1:8000. Host ports 8080/8100 are taken by other services.
## Spec
- Canonical invariants and task status live in `SPEC.md` (§V invariants, §T tasks, §B bug log). Keep it updated when behavior changes; check §V before modifying.