diff --git a/README.md b/README.md index e9f5a1a..8dd7b38 100644 --- a/README.md +++ b/README.md @@ -239,8 +239,18 @@ lb fix-issue-123 # cd ~/dev/lnbits/fix-issue-123 - [`docs/lnbits-workspace-notes.md`](docs/lnbits-workspace-notes.md) — practical gotchas and design constraints for day-to-day lnbits work: port choice, `LNBITS_SRC` build-context traps, the - extension-folder-upgrade-wipes-fork issue, Nostr key handling, - CLINK protocol scope, fork versioning. + extension-folder-upgrade-wipes-fork issue, settings precedence + (`.env` vs DB), Nostr key handling, CLINK protocol scope, fork + versioning. +- [`docs/lnbits-extension-dev.md`](docs/lnbits-extension-dev.md) — + building and maintaining LNbits extensions: auth-decorator + distinctions, FakeWallet vs regtest testing strategy, the + `migrations_fork.py` pattern for keeping fork-only schema deltas + out of upstream-tracked `migrations.py`. +- [`docs/lnbits-frontend-gotchas.md`](docs/lnbits-frontend-gotchas.md) — + Vue/Quasar UMD traps in lnbits page templates: no self-closing + tags, CSS specificity vs Quasar's `!important` utilities, cache + busting via `?v={server_startup_time}`, dark-mode color discipline. ## Contributing to this scaffold diff --git a/docs/lnbits-extension-dev.md b/docs/lnbits-extension-dev.md new file mode 100644 index 0000000..3f7e27c --- /dev/null +++ b/docs/lnbits-extension-dev.md @@ -0,0 +1,143 @@ +# lnbits extension development + +Reference for building and maintaining LNbits extensions — the parts +that catch first-time extension authors and the patterns worth +adopting once you maintain a fork-modified extension. + +## Auth decorators + +Easy to confuse. Different scopes: + +| Decorator | Auth scope | Returns | Use for | +|---|---|---|---| +| `require_invoice_key` | Wallet invoice key — read access | `Wallet` | Read endpoints (balance, payment history) callable with a user's invoice key | +| `require_admin_key` | Wallet admin key — write to **own wallet only** | `Wallet` | Endpoints that create payments / modify wallet state | +| `check_admin` | **LNbits instance admin** (super_user + `lnbits_admin_users`), Bearer token | `Account` | Cross-user / admin-only operations | +| `check_super_user` | LNbits super user only, Bearer token | `Account` | Operations restricted to the single super_user | + +**`require_admin_key` ≠ `check_admin`.** This is the most common +misuse. `require_admin_key` is a *wallet*-level write key — every +user has one for each of their own wallets. `check_admin` is +*instance* admin access. Endpoints that operate on other users' data +or change global settings need `check_admin`, not `require_admin_key`. + +## Testing + +Use **FakeWallet** +(`LNBITS_BACKEND_WALLET_CLASS=FakeWallet`) for testing extension CRUD, +API endpoints, and UI changes. Spin up the full regtest stack only +when end-to-end Lightning payment behavior is actually under test. + +**Why:** regtest takes time to start (docker build + multiple LND/CLN +containers), uses real resources, and adds no fidelity for non-payment +flows. FakeWallet makes payments succeed instantly with no network — +ideal for testing the surrounding logic. + +If you ship a `dev` CLI (see lnbits-sensei's pattern), keep +`--fakewallet` as the default mode for this reason. + +## Fork-migrations pattern (`migrations_fork.py`) + +Long-lived forks need to add schema columns and tables on top of +upstream's. The naive approach — append fork-only migration functions +to `migrations.py` — guarantees merge conflicts on every upstream +rebase. The pattern below sidesteps that by keeping `migrations.py` +byte-identical to upstream and putting fork deltas in a sibling file. + +> **Note:** this pattern requires a patch to LNbits core's +> `migrate_extension_database()` that loads `migrations_fork.py` under +> a `_fork` key in the `dbversions` table. As of 2026 that +> patch is fork-internal (an upstream PR is the natural follow-up). +> If you maintain a fork that ships this patch, the rest of this +> section is the user-facing pattern. + +### Architecture facts + +- `dbversions` lives in the **core LNbits DB** (`database.sqlite3`), + not per-extension. Schema: `(db TEXT PRIMARY KEY, version INT)`. + `update_migration_version` is an INSERT-OR-UPDATE, so a new + `_fork` row appears on first run with no schema migration + needed core-side. +- Extension data tables live in `ext_.sqlite3` (SQLite) or a + Postgres schema named after ``. Created lazily on first + `Database.connect()` via `ATTACH` (SQLite) or `CREATE SCHEMA` + (Postgres). +- The version-bump connection is routed based on `db.schema`: `None` + means a core migration (same connection), set means an extension + (opens a fresh `core_db.connect()` to write `dbversions`). See + `lnbits/core/helpers.py:run_migration`. +- **No cross-DB atomicity.** Extension migration writes commit to + `ext_.sqlite3`; the `dbversions` upsert commits to + `database.sqlite3`. If the extension write succeeds and the + dbversions write fails, the migration is orphaned — re-runs on + next startup. **Every fork migration MUST be idempotent** (use + an `_alter_add_column_safe` wrapper that swallows + duplicate-column errors, `CREATE TABLE IF NOT EXISTS`, etc.). + Self-healing covers the orphan case. + +### Squash recipe (adopting the pattern on an existing fork) + +If your fork has accumulated fork-only migrations interleaved into +`migrations.py`: + +1. **Restore `migrations.py` to upstream-byte-identical content.** + Drop all fork-only `m{NNN}_*` functions and any helper added only + for them. +2. **Create `migrations_fork.py`** with a SINGLE + `m001___schema` function that idempotently applies + every fork-only schema delta the old migrations used to do. One + readable file forever. +3. The squash uses `_alter_add_column_safe` per ALTER and + `CREATE TABLE IF NOT EXISTS` per table — no-ops cleanly on + installs that already ran the old fork migrations. + +### One-time fix on existing installs adopting the pattern + +Installs that previously ran the old fork migrations have their +`dbversions['']` row ahead of upstream (e.g. `events|11` after +fork-only `m007`–`m011`). After moving to `migrations_fork`, the +next upstream rebase that adds e.g. `m007_add_allow_fiat` would +compare `7 > 11 → false` and **silently skip** the new upstream +migration. + +**Reset the row to match upstream's actual migration count** before +the rebase lands: + +```sql +-- Run against the CORE DB (database.sqlite3), not the extension DB. +UPDATE dbversions SET version = WHERE db = ''; +``` + +For containerized dev where the file is root-owned inside the +container: + +```bash +docker compose exec lnbits python3 -c "import sqlite3; \ + c = sqlite3.connect('database.sqlite3'); \ + c.execute(\"UPDATE dbversions SET version = WHERE db = ''\"); \ + c.commit()" +``` + +### Upstream-overlap scenario at rebase time + +If upstream eventually adds a schema change you already carry in +`migrations_fork.py` (e.g. they add the same `ALTER TABLE … ADD COLUMN +bar` you shipped), the next rebase creates a problem: fresh installs +work (your `_alter_add_column_safe` guards swallow the dup), but +**existing installs crash** when upstream's now-redundant migration +runs without an idempotency guard (upstream rarely uses them). + +Mitigation at rebase time: + +1. **Prune the redundant block from `migrations_fork.py`** so future + fresh installs get the column from upstream's migration. +2. **Pre-deploy `dbversions` surgery on every affected install**: + `UPDATE dbversions SET version = WHERE db = ''` + so the loader skips the now-overlapping upstream migration. + +Do both — (1) keeps future fresh installs clean, (2) keeps existing +installs alive through the deploy. + +**Don't** patch upstream's migration with idempotency guards in your +fork. That breaks the "migrations.py == upstream byte-identical" +property and reintroduces every-rebase conflicts. diff --git a/docs/lnbits-frontend-gotchas.md b/docs/lnbits-frontend-gotchas.md new file mode 100644 index 0000000..cbd1b7b --- /dev/null +++ b/docs/lnbits-frontend-gotchas.md @@ -0,0 +1,105 @@ +# lnbits frontend gotchas + +LNbits ships its UI with **Vue 3 + Quasar 2 as UMD globals** — no +build step, plain Jinja templates with per-page JS. This applies +across lnbits core (`lnbits/templates/*.html`), every extension +(`/templates/`), and every fork that doesn't restructure the +frontend stack. The UMD load model has several traps that don't +manifest under the build-step model most Vue tutorials assume. + +## No self-closing tags + +Per [Quasar's UMD usage rules](https://quasar.dev/start/umd/#usage), +components must use the explicit-close form: + +```html + + + + + + + +``` + +**Why:** UMD-loaded templates are parsed by the browser's HTML parser, +not Vue's compiler. The HTML parser doesn't honor self-close on +non-void elements (per the HTML spec). The close tag gets implied at +the wrong place, nesting breaks silently, and subsequent siblings end +up nested inside the prior component. + +Self-closing is fine in `.vue` SFCs (the build step rewrites them +before the browser sees anything), so if you copy a snippet from a +Vue SFC repo into an LNbits template, **expand all self-closing tags +before saving**. + +## CSS specificity trap + +LNbits applies its own theme overrides on Quasar's typography +utilities (`.text-caption`, `.text-grey-*`, etc.) with `!important`. +Class-based CSS rules in an extension page — *even with `!important`* — +lose this fight unless your selector is strictly more specific than +the upstream rule. + +**Rule:** for per-element typography/color overrides on LNbits pages, +reach for Vue `:style` bindings (or static `style="..."` attrs), not +` + + + + +``` + +Background/border tweaks at card-level via class are fine — the trap +is specifically the typography utilities (`text-*`) and Quasar's +color utilities. + +## Cache busting + +Static assets are served with `?v={server_startup_time}` appended +(see `static_url_for` in `lnbits/helpers.py`). Consequences: + +- **Bumping JS requires a server restart.** Reloading the browser + doesn't help if `?v=` hasn't changed — the browser keeps serving + the cached file. +- **Jinja templates re-render on every request** (the `?v=` is only + on static assets). No restart needed for template edits — just + refresh. + +If a browser keeps serving stale JS after a restart, hard-refresh +(`Ctrl+Shift+R`) to bypass the HTTP cache. + +## Dark-mode color discipline + +LNbits's dark theme inverts text colors on most surfaces but **not** +on `bg-{color}-1` pale-background utilities. Result: a `bg-red-1` +without an explicit text color renders white-on-cream under dark +theme — basically invisible. + +**Rule:** pair every pale-background utility with an explicit dark +text class: + +```html + +
Warning
+ + +
Warning
+``` + +Same for `bg-green-1`, `bg-blue-1`, `bg-amber-1`, etc. The `text-grey-9` +choice is the safe default; pick a darker shade if you want stronger +contrast. + +## When in doubt + +Test under both light and dark themes (Quasar's theme toggle is at +the top of every LNbits page once you're logged in). Most of the +above gotchas are silent under one theme and obvious under the other +— don't ship UI without flipping the toggle at least once.