Commit graph

9 commits

Author SHA1 Message Date
d29d4dbec9 feat(signer): migrate Nostr publishing off account.prvkey → resolve_for_wallet (#11)
Closes aiolabs/restaurant#11. Pre-cascade prerequisite for
aiolabs/lnbits#17 (signer abstraction phase 1), which lands an m002
startup job that NULLs the legacy `accounts.prvkey` column. After
this migration, the restaurant extension reads no plaintext nsec and
works with any NostrSigner backend (LocalSigner / RemoteBunkerSigner
/ ClientSideOnlySigner).

## What changed

### views_api.py — _resolve_signing_keypair → _resolve_signer

Was: `_resolve_signing_keypair(restaurant)` returned `(pubkey, prvkey)`
read directly from `account.pubkey` / `account.prvkey` after walking
wallet → account.

Now: `_resolve_signer(restaurant)` returns `NostrSigner | None`.
Precedence order preserved:

  1. `restaurant.nostr_pubkey` set → per-restaurant identity. Still
     a no-op TODO returning None until a per-restaurant signer /
     vault ships (separate concern, future work).
  2. fallback → `resolve_for_wallet(restaurant.wallet)` (the DRY
     helper from aiolabs/lnbits#23 — wallet → account → signer →
     can_sign-check in one call, returns None on any soft-fail).

Three call sites updated (`_publish_restaurant`, `_publish_menu_item`,
`_publish_menu_item_delete`): each now passes the resolved `signer`
to `publish_event` instead of the keypair tuple, and uses
`signer.pubkey` for tag construction. The discovery-echo line in
`_publish_restaurant` (`restaurant.nostr_pubkey = signer.pubkey`)
preserves prior behavior.

Dropped now-unused imports: `get_account`, `get_wallet`.

### nostr_publisher.py — publish_event

Was: `publish_event(client, event, private_key_hex)` called a local
`sign_nostr_event` helper that signed in place via
`coincurve.PrivateKey.sign_schnorr`.

Now: `publish_event(client, event, signer: NostrSigner)` builds the
unsigned dict (`kind`/`created_at`/`tags`/`content`), hands it to
`await signer.sign_event(...)`, and writes `id`/`pubkey`/`sig` back
onto the local `NostrEvent` model before publishing. The signer
backend (LocalSigner / RemoteBunkerSigner) is transparent.

Removed the `sign_nostr_event` helper entirely — the signer
abstraction handles all signing now.

Dropped the `coincurve` import; no direct crypto in this extension.

### docs/nostr-layer.md — signing prose

Updated the Signing section to reflect the signer-abstraction model:
`resolve_for_wallet` resolves a `NostrSigner`, the extension no
longer touches `account.prvkey` or calls `coincurve.sign_schnorr`
directly. The per-restaurant-identity TODO is preserved.

## Acceptance

- [x] `_resolve_signing_keypair` replaced with `_resolve_signer` returning NostrSigner
- [x] `sign_nostr_event` helper removed (signer handles it internally)
- [x] `publish_event` accepts a NostrSigner instead of private_key_hex
- [x] all three call sites updated to pass the signer
- [x] re-grep `restaurant/`: zero `account.prvkey` references
- [x] coincurve import dropped
- [x] docs/nostr-layer.md updated in the same commit

Manual smoke testing + tag + catalog entry follow the migration
landing; will run against the regtest stack with lnbits on
`issue-18-phase-2.3` (which validates both LocalSigner and
RemoteBunkerSigner signing paths end-to-end).

## Cross-references

- aiolabs/restaurant#11 — issue this commit closes
- aiolabs/lnbits#17 — the cascading signer-abstraction PR
- aiolabs/lnbits#23 — the resolve_for_wallet helper this uses
- aiolabs/lnbits#21 — umbrella audit (5 affected extensions)
- aiolabs/events#23 / aiolabs/tasks#3 — sister migrations (already on signer-abstraction branches)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 22:26:41 +02:00
42746d7321 fix(cms): KDS card text legible on dark mode
The age-escalation highlights (bg-amber-1 / bg-orange-1 /
bg-red-1) are very pale Quasar shades. On LNbits dark mode the
q-card inherits a near-white text color from the theme — paired
with the pale background that's white-on-cream, which is what
the user reported: the '1x Coffee' on a ready-card was barely
visible.

Pin an explicit text-grey-9 alongside each pale bg so dark text
on light background renders in both themes. The 'no highlight'
branch returns '' unchanged, so non-aged orders still use the
q-card's theme-aware default text color.
2026-05-11 19:18:33 +02:00
638f36e945 fix(services): convert fiat menu prices to sat via exchange rates
Before this fix `_to_msat(item.price)` blindly did `price * 1000`,
treating any menu price as sat-denominated regardless of the item's
`currency` field. Quote and bolt11 were internally consistent but
charged ~0.1% of the real price for fiat-priced menus.

  Big Jay's seeded with GTQ:
    2× Tacos (Maíz, +Brisket, +Chicken)
    pre-fix:  170 GTQ → 170000 msat → 170 sat invoice (~$0.14)
    post-fix: 170 GTQ → 26968000 msat → 26968 sat invoice (~$22)

services.py:
  - Drop `_to_msat` in favor of a `_price_to_msat(amount, currency)`
    helper. Sat-aliased currencies ("sat", "sats", "satoshi",
    "msat", …) take the flat ×1000 path; everything else round-
    trips through lnbits.utils.exchange_rates.fiat_amount_as_satoshis
    (same pool the events extension uses).
  - Update _price_line_item: item.price AND each modifier.price_delta
    are converted using item.currency. Modifier deltas inherit the
    parent item's currency since we don't carry a per-modifier
    currency field.
  - Update quote_balance_required: same conversion via the item's
    currency.

Verified live against the seeded "Big Jay's Bustaurant":
  GTQ → sat conversion matches LNbits's bitcoin-price aggregate
  (Binance / Blockchain / Bitfinex / Bitstamp / Coinbase / yadio).
  Quote returns 26968 sats for 170 GTQ — within ~2% of expected
  rate from external sources.
2026-05-11 19:18:16 +02:00
6dae57f3f4 feat(api): public GET /restaurants/by-slug/{slug}
Prerequisite for the customer webapp module (aiolabs/webapp,
branch feat/restaurant-bundle): the webapp's /r/:slug route needs
to resolve a slug to a Restaurant payload without an admin key.

crud.get_restaurant_by_slug already exists (used by the server-
rendered CMS routes in views.py); just expose it as a public REST
endpoint. Mirrors api_get_restaurant by id and is declared before
the bare-id route so the static prefix wins FastAPI's path match.

Verified live against seeded 'Big Jay's Bustaurant':
  GET /restaurant/api/v1/restaurants/by-slug/big-jays-bustaurant
  -> 200 with the Restaurant payload.
2026-05-11 19:17:35 +02:00
Padreug
dd756ecfc3 docs: update NIPs path to ~/dev/refs/repos/nostr-protocol/nips 2026-05-09 07:11:06 +02:00
e2a2f4a633 docs: rename castle → libra in design-conversation example
The castle LNbits extension was renamed to libra. Updating the
parallel-extension reference in the design-conversation notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 07:11:06 +02:00
42a8b08a5b docs: Obsidian-style vault under docs/
Add a navigable Obsidian vault as the project's first-class
technical documentation. Notes cross-reference with [[wikilinks]];
docs/index.md is the Map of Content.

New notes:
  index.md             MOC, entry point
  architecture.md      what the extension owns vs what lives outside
  data-model.md        entity-by-entity schema reference
  menu-tree.md         the arbitrary-depth tree concept
  order-flow.md        state machine + invoice listener + print
  nostr-layer.md       kinds 0/30402/5/1059, signing, t-tags
  api-reference.md     endpoint catalog by audience
  cms.md               Vue 3 + Quasar 2 UMD conventions, q-tree
  webapp-integration.md  multi-restaurant cart pattern + atomicity
  glossary.md          domain terms

Existing notes (kept as-is):
  adr-0001-menu-tree.md  the storage choice rationale
  design-conversation.md trimmed transcript

README.md adds a Documentation section pointing at docs/index.md
with the headline note list. Each note links to ~3-5 others; the
vault forms a connected graph.

A project-level memory rule (saved outside the repo) commits us to
keeping these docs in sync as the code evolves: any commit that
materially changes schema, API, order flow, Nostr surface, CMS
conventions, or webapp integration must update the relevant note(s)
in the same commit.
2026-05-09 07:11:06 +02:00
7f7915a041 docs: README + ADR for menu tree refactor
README.md
  - Update intro: 'menu tree' is now arbitrary-depth (cap 4
    levels), items can attach to any node.
  - Update Nostr publisher description to mention ancestor 't'
    tags (slugified, root-first) so clients can filter on
    #t=hot-beverages, #t=coffee-based, etc.
  - Replace the Data model table's categories/subcategories rows
    with a single menu_nodes row that explains the adjacency-list
    + materialized-path + depth shape and points at the ADR.
  - Replace the boilerplate 'full CRUD for categories,
    subcategories, ...' line with a real menu_nodes API list,
    including the cascade-detach behavior on delete and the
    rename-triggers-subtree-republish behavior on update.

docs/adr-0001-menu-tree.md
  - New ADR explaining the storage choice (adjacency list +
    materialized path + denormalized depth), the alternatives
    considered (closure table, Postgres ltree, pure adjacency,
    nested set), and the consequences. Provides the rationale
    so future contributors don't relitigate the decision.
2026-05-09 07:11:06 +02:00
60d59c0370 docs: add design-conversation transcript
Trimmed render of the Claude Code session that produced the initial
scaffold. Tool calls, agent sub-conversations, system reminders, and
diagnostics stripped; only user prompts + assistant prose remain
(31 user / 30 assistant turns, ~32 KB).

Captures the design rationale that doesn't otherwise live in commit
messages:
- why per-restaurant invoices instead of split settlement (each
  item linked to its restaurant; each restaurant issues its own
  bolt11; webapp pre-flights total balance before paying any)
- why 'festival' is not an entity in the data model (curated
  NIP-51 lists are emergent from outside the extension)
- why menus are NIP-99 listings (parameterized replaceable; tags
  carry price, dietary, allergens, ingredients)
- why the customer kiosk lives in ~/dev/webapp, not in this
  extension's static dir
- the 'nostrize everything' direction from the OmniXY stack
  overview, and how it shapes the publisher/sync split here
2026-05-09 07:11:06 +02:00