Operator branding — logo, title, color scheme overrides #47

Open
opened 2026-06-13 22:03:01 +00:00 by padreug · 0 comments
Owner

Migrated from aiolabs/lamassu-next#47 — opened by @padreug on 2026-05-24.\n\nOperators running bitSpire (and its planned operator dashboard, satmachineadmin) should be able to swap the default branding on a deployed ATM without rebuilding the image. Today these are hardcoded:

  • apps/machine/public/logo.png — baked into the nix-store closure
  • apps/machine/src/views/IdleView.vue:72 — title (Bitcoinmat)
  • apps/machine/src/style.css — color scheme (currently defaults to gruvbox :root; 6 built-in [data-theme='...'] palettes available)

This conflicts with the "identity-free image" property of the deploy pipeline (docs/machine-installation.md): one image flavor should serve every operator, with per-deployment variation living in /var/lib/bitspire/ and provisioned via provision-atm.sh. Defaults baked into the build apply when no override is present.

Branch scope: dev (bitSpire). Paths use /var/lib/bitspire/...; service is bitspire.service.

V1 scope — dual-source

Branding has TWO concurrent sources. Source-of-truth priority (highest wins):

  1. @bitSpire/branding Nostr event from satmachineadmin — operator pubkey publishes a replaceable event (kind + d-tag specified in follow-up issue). The ATM subscribes on boot; subsequent updates apply live.
  2. Local file at /var/lib/bitspire/branding/ — logo.png + logo-dark.png + branding.json deposited on disk by the operator (e.g. via provision-branding.sh).
  3. Built-in defaults — current Bitcoinmat / /logo.png / gruvbox baseline.

The ATM-side consumer is structured as a BrandingSource interface so the Nostr path can be wired without touching renderer code. Local-file is implemented first (faster to ship, no hard dep on satmachineadmin work); the Nostr path lands once the corresponding satmachineadmin work is done (follow-up issue).

Schema

The local-file source uses three sibling files under /var/lib/bitspire/branding/. Any of them is optional; absent files fall back to defaults.

  • logo.png — operator logo, rendered at ~12vh on the idle screen
  • logo-dark.png — optional dark-mode variant; renderer auto-switches based on the effective color mode (resolves colorMode = system via prefers-color-scheme). Falls back to logo.png when absent
  • branding.json — JSON with these optional fields:
    • title (string, default "Bitcoinmat")
    • theme — one of "gruvbox", "catppuccin", "cyberpunk", "dracula", "nord", "tokyo-night", or "custom"
    • custom_colors — when theme: "custom", an object mapping CSS-var names to hex values. Supports any of: --background, --foreground, --card, --card-foreground, --popover, --popover-foreground, --primary, --primary-foreground, --secondary, --secondary-foreground, --muted, --muted-foreground, --accent, --accent-foreground, --destructive, --border, --input, --ring, --success, --success-foreground, --warning, --warning-foreground, --bitcoin, --bitcoin-foreground, --qr, --qr-foreground, --radius. Operator can override a subset; unset vars fall back to gruvbox defaults.
    • Optional custom_colors.dark — same shape, applied under .dark mode

The Nostr-event source (V2) will carry the same logical payload; binary logos go inline as base64 (or as an https:// URL the ATM dereferences) since Nostr events can't reference sibling files.

Plumbing

Electron main process exposes a branding object via the existing get-config IPC, with both logoDataUrl and logoDarkDataUrl fields (base64 PNGs or null). The renderer applies title + theme + (for theme: "custom") injects a <style id="custom-theme"> block setting the chosen vars on :root and .dark. Logos bind to an <img :src> reactive ref that switches between light/dark variants based on the effective color mode — so live Nostr updates AND OS-level dark-mode toggles propagate without a reload.

Provisioning (local-file source)

New deploy/nixos/provision-branding.sh: rsyncs a local <dir> into /var/lib/bitspire/branding/ on the ATM and restarts bitspire.service. Composes with provision-atm.sh rather than overloading it (the existing script bundles dev-only QEMU credential fetching and is awkward to extend with a --branding-dir flag).

Future considerations (decide when we pick this up)

To weigh after bitSpire + satmachineadmin dashboard are finished:

  • Tagline badges — currently hardcoded No KYC / No Registration / Just Bitcoin (IdleView.vue:73-82). Could be an array of {label, variant} in branding.json.
  • Support page contact info — operator name, phone, email, support URL, Telegram/Nostr/Signal handles on /support. Probably needed for any real fleet deployment.
  • Receipt header/footer text — operator legal name + address (some jurisdictions require this on cash-handling receipts).
  • Fee disclosure copy — custom text shown alongside the buy/sell percentages on the idle screen.
  • Buy/Sell button copy — currently Buy Bitcoin / Sell Bitcoin plus subtitles (IdleView.vue:127-142). Operator might want localized or different wording.
  • Idle-screen subtitle / welcome line — a free-form line below the title.
  • Default mode (light/dark) — operator preference for which mode the kiosk starts in.
  • Locale / language — full i18n is a bigger project but worth flagging as a downstream branding concern.
  • Animation toggle — ties into the Sintra perf note; operator can disable continuous animations regardless of hardware.
  • Idle-screen background — operator-supplied background image or color override.
  • Sound effects on/off + custom pack — coin/bill insertion feedback sounds.
  • QR-code link to operator's support channel — e.g., scannable "Need help? scan this" code linking to the operator's Telegram bot or Nostr profile.
  • Custom favicon / OS-level branding — Electron window title, taskbar icon (mostly invisible in kiosk mode but worth noting).
  • Hardware diagram on the help page — operator-supplied photos showing where bills go in / come out (useful when retrofitting older chassis).

Cross-product note

The eventual source-of-truth for branding is satmachineadmin acting as the bitSpire operator dashboard (see follow-up issue). Operator uploads logo / picks theme / sets title once in the LNbits admin UI; satmachineadmin publishes a replaceable Nostr event keyed on operator pubkey; every bitSpire ATM tied to that operator subscribes and applies live. This matches the "no HTTP to the Lightning backend" / "Nostr-only comms" direction documented in CLAUDE.md on dev and avoids per-ATM rsync entirely once it lands. The local-file source remains as a bootstrap path and an offline-recovery escape hatch.

Acceptance

  • ATM with no branding overrides (no file, no Nostr event) renders identically to today
  • Local-file: branding.json with theme: "catppuccin" + custom title + logo.png in /var/lib/bitspire/branding/ renders the override
  • Local-file: dropping logo-dark.png in addition to logo.png swaps logos when the kiosk's color mode is dark (verified by toggling the light/dark button)
  • Local-file: branding.json with theme: "custom" and a partial custom_colors map applies overrides on top of gruvbox defaults
  • provision-branding.sh ./acme-brand <ip> deploys branding folder and restarts bitspire.service
  • Override takes effect on next systemctl restart bitspire (no rebuild needed)
  • BrandingSource interface in place so the Nostr-event source can be added without rewriting renderer consumers
  • When BOTH a local file AND a Nostr event are present, the Nostr event wins
> _Migrated from [aiolabs/lamassu-next#47](https://git.atitlan.io/aiolabs/lamassu-next/issues/47) — opened by @padreug on 2026-05-24._\n\nOperators running bitSpire (and its planned operator dashboard, satmachineadmin) should be able to swap the default branding on a deployed ATM without rebuilding the image. Today these are hardcoded: - `apps/machine/public/logo.png` — baked into the nix-store closure - `apps/machine/src/views/IdleView.vue:72` — title (`Bitcoinmat`) - `apps/machine/src/style.css` — color scheme (currently defaults to gruvbox `:root`; 6 built-in `[data-theme='...']` palettes available) This conflicts with the "identity-free image" property of the deploy pipeline (`docs/machine-installation.md`): one image flavor should serve every operator, with per-deployment variation living in `/var/lib/bitspire/` and provisioned via `provision-atm.sh`. Defaults baked into the build apply when no override is present. > Branch scope: `dev` (bitSpire). Paths use `/var/lib/bitspire/...`; service is `bitspire.service`. ### V1 scope — dual-source Branding has TWO concurrent sources. **Source-of-truth priority (highest wins):** 1. **`@bitSpire/branding` Nostr event from satmachineadmin** — operator pubkey publishes a replaceable event (kind + d-tag specified in follow-up issue). The ATM subscribes on boot; subsequent updates apply live. 2. **Local file at `/var/lib/bitspire/branding/`** — `logo.png` + `logo-dark.png` + `branding.json` deposited on disk by the operator (e.g. via `provision-branding.sh`). 3. **Built-in defaults** — current `Bitcoinmat` / `/logo.png` / gruvbox baseline. The ATM-side consumer is structured as a `BrandingSource` interface so the Nostr path can be wired without touching renderer code. Local-file is implemented first (faster to ship, no hard dep on satmachineadmin work); the Nostr path lands once the corresponding satmachineadmin work is done (follow-up issue). ### Schema The local-file source uses three sibling files under `/var/lib/bitspire/branding/`. Any of them is optional; absent files fall back to defaults. - **`logo.png`** — operator logo, rendered at ~12vh on the idle screen - **`logo-dark.png`** — optional dark-mode variant; renderer auto-switches based on the effective color mode (resolves `colorMode = system` via `prefers-color-scheme`). Falls back to `logo.png` when absent - **`branding.json`** — JSON with these optional fields: - `title` (string, default `"Bitcoinmat"`) - `theme` — one of `"gruvbox"`, `"catppuccin"`, `"cyberpunk"`, `"dracula"`, `"nord"`, `"tokyo-night"`, **or** `"custom"` - `custom_colors` — when `theme: "custom"`, an object mapping CSS-var names to hex values. Supports any of: `--background`, `--foreground`, `--card`, `--card-foreground`, `--popover`, `--popover-foreground`, `--primary`, `--primary-foreground`, `--secondary`, `--secondary-foreground`, `--muted`, `--muted-foreground`, `--accent`, `--accent-foreground`, `--destructive`, `--border`, `--input`, `--ring`, `--success`, `--success-foreground`, `--warning`, `--warning-foreground`, `--bitcoin`, `--bitcoin-foreground`, `--qr`, `--qr-foreground`, `--radius`. Operator can override a subset; unset vars fall back to gruvbox defaults. - Optional `custom_colors.dark` — same shape, applied under `.dark` mode The Nostr-event source (V2) will carry the same logical payload; binary logos go inline as base64 (or as an `https://` URL the ATM dereferences) since Nostr events can't reference sibling files. ### Plumbing Electron main process exposes a `branding` object via the existing `get-config` IPC, with both `logoDataUrl` and `logoDarkDataUrl` fields (base64 PNGs or null). The renderer applies `title` + `theme` + (for `theme: "custom"`) injects a `<style id="custom-theme">` block setting the chosen vars on `:root` and `.dark`. Logos bind to an `<img :src>` reactive ref that switches between light/dark variants based on the effective color mode — so live Nostr updates AND OS-level dark-mode toggles propagate without a reload. ### Provisioning (local-file source) New `deploy/nixos/provision-branding.sh`: rsyncs a local `<dir>` into `/var/lib/bitspire/branding/` on the ATM and restarts `bitspire.service`. Composes with `provision-atm.sh` rather than overloading it (the existing script bundles dev-only QEMU credential fetching and is awkward to extend with a `--branding-dir` flag). ### Future considerations (decide when we pick this up) To weigh after bitSpire + satmachineadmin dashboard are finished: - **Tagline badges** — currently hardcoded `No KYC` / `No Registration` / `Just Bitcoin` (`IdleView.vue:73-82`). Could be an array of `{label, variant}` in `branding.json`. - **Support page contact info** — operator name, phone, email, support URL, Telegram/Nostr/Signal handles on `/support`. Probably needed for any real fleet deployment. - **Receipt header/footer text** — operator legal name + address (some jurisdictions require this on cash-handling receipts). - **Fee disclosure copy** — custom text shown alongside the buy/sell percentages on the idle screen. - **Buy/Sell button copy** — currently `Buy Bitcoin` / `Sell Bitcoin` plus subtitles (`IdleView.vue:127-142`). Operator might want localized or different wording. - **Idle-screen subtitle / welcome line** — a free-form line below the title. - **Default mode (light/dark)** — operator preference for which mode the kiosk starts in. - **Locale / language** — full i18n is a bigger project but worth flagging as a downstream branding concern. - **Animation toggle** — ties into the Sintra perf note; operator can disable continuous animations regardless of hardware. - **Idle-screen background** — operator-supplied background image or color override. - **Sound effects on/off + custom pack** — coin/bill insertion feedback sounds. - **QR-code link to operator's support channel** — e.g., scannable "Need help? scan this" code linking to the operator's Telegram bot or Nostr profile. - **Custom favicon / OS-level branding** — Electron window title, taskbar icon (mostly invisible in kiosk mode but worth noting). - **Hardware diagram on the help page** — operator-supplied photos showing where bills go in / come out (useful when retrofitting older chassis). ### Cross-product note The eventual source-of-truth for branding is **satmachineadmin acting as the bitSpire operator dashboard** (see follow-up issue). Operator uploads logo / picks theme / sets title once in the LNbits admin UI; satmachineadmin publishes a replaceable Nostr event keyed on operator pubkey; every bitSpire ATM tied to that operator subscribes and applies live. This matches the "no HTTP to the Lightning backend" / "Nostr-only comms" direction documented in `CLAUDE.md` on `dev` and avoids per-ATM rsync entirely once it lands. The local-file source remains as a bootstrap path and an offline-recovery escape hatch. ### Acceptance - [ ] ATM with no branding overrides (no file, no Nostr event) renders identically to today - [ ] Local-file: `branding.json` with `theme: "catppuccin"` + custom `title` + `logo.png` in `/var/lib/bitspire/branding/` renders the override - [ ] Local-file: dropping `logo-dark.png` in addition to `logo.png` swaps logos when the kiosk's color mode is dark (verified by toggling the light/dark button) - [ ] Local-file: `branding.json` with `theme: "custom"` and a partial `custom_colors` map applies overrides on top of gruvbox defaults - [ ] `provision-branding.sh ./acme-brand <ip>` deploys branding folder and restarts `bitspire.service` - [ ] Override takes effect on next `systemctl restart bitspire` (no rebuild needed) - [ ] `BrandingSource` interface in place so the Nostr-event source can be added without rewriting renderer consumers - [ ] When BOTH a local file AND a Nostr event are present, the Nostr event wins
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/bitspire#47
No description provided.