Adds back `services.webapp.appName` (briefly removed in the previous commit per #100's strict policy) so existing hosts keep working without authoring full per-host brand kits. Resolution order: 1. If brandDir is set, use it (full brand kit wins). 2. Else synthesize: take the flake's default logo + a brand.json carrying appName as both name and shortName. This is NOT a VITE_APP_NAME env shim — it threads through the brand kit pipeline properly (brand.json read by vite-branding.ts). Hosts that need themed chrome / their own logo upgrade to a real brandDir. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
306 lines
10 KiB
Nix
306 lines
10 KiB
Nix
# NixOS module for serving the Vue 3 webapp.
|
|
#
|
|
# Build is delegated to the webapp flake's lib.mkWebapp (since
|
|
# aiolabs/webapp#97). This module owns:
|
|
# - the nginx vhost
|
|
# - the per-deployment VITE_* env vars (relays, LNbits URL, hub
|
|
# tile URLs, …) that get baked into the bundle
|
|
# - brand kit selection via `brandDir`
|
|
#
|
|
# Per-deployment name customization flows through brand.json under
|
|
# `brandDir`, NOT through a `VITE_APP_NAME` knob — see strict policy
|
|
# landed in aiolabs/webapp#99 / #100.
|
|
{ config, lib, pkgs, ... }:
|
|
|
|
let
|
|
cfg = config.services.webapp;
|
|
|
|
# Synthesize a brand kit from `appName` when no explicit brandDir is
|
|
# provided: take the flake's default logo and overlay a brand.json
|
|
# carrying the host's app name. This lets existing hosts keep
|
|
# `services.webapp.appName = "Bouge"` and get "Bouge" in the manifest
|
|
# WITHOUT needing to author a full brand kit per host. Hosts that DO
|
|
# author a brand kit (logo, theme colors) supply `brandDir` directly
|
|
# and `appName` becomes informational.
|
|
synthesizedBrand = pkgs.runCommand "aio-webapp-brand-${cfg.appName}" {} ''
|
|
mkdir -p $out
|
|
cp ${cfg.flake}/branding/default/logo.png $out/logo.png
|
|
cat > $out/brand.json <<EOF
|
|
{"name":${builtins.toJSON cfg.appName},"shortName":${builtins.toJSON cfg.appName}}
|
|
EOF
|
|
'';
|
|
|
|
resolvedBrandDir =
|
|
if cfg.brandDir != null then cfg.brandDir
|
|
else synthesizedBrand;
|
|
|
|
webapp = cfg.flake.lib.mkWebapp {
|
|
inherit pkgs;
|
|
brandDir = resolvedBrandDir;
|
|
extraEnv = {
|
|
ELECTRON_SKIP_BINARY_DOWNLOAD = "1";
|
|
NODE_ENV = "production";
|
|
|
|
VITE_NOSTR_RELAYS = builtins.toJSON cfg.nostrRelays;
|
|
VITE_ADMIN_PUBKEYS = builtins.toJSON cfg.adminPubkeys;
|
|
|
|
VITE_LNBITS_BASE_URL = cfg.lnbitsBaseUrl;
|
|
VITE_LNBITS_DEBUG = if cfg.lnbitsDebug then "true" else "false";
|
|
VITE_LNBITS_NOSTR_TRANSPORT_PUBKEY = cfg.lnbitsNostrTransportPubkey;
|
|
VITE_WEBSOCKET_ENABLED = if cfg.websocketEnabled then "true" else "false";
|
|
|
|
VITE_LIGHTNING_DOMAIN = cfg.lightningDomain;
|
|
|
|
VITE_VAPID_PUBLIC_KEY = cfg.vapidPublicKey;
|
|
VITE_PUSH_NOTIFICATIONS_ENABLED = if cfg.pushNotificationsEnabled then "true" else "false";
|
|
|
|
VITE_PICTRS_BASE_URL = cfg.pictrsBaseUrl;
|
|
VITE_MARKET_NADDR = cfg.marketNaddr;
|
|
VITE_DEMO_MODE = if cfg.demoMode then "true" else "false";
|
|
|
|
VITE_HUB_EVENTS_URL = cfg.hubEventsUrl;
|
|
VITE_HUB_LIBRA_URL = cfg.hubLibraUrl;
|
|
VITE_HUB_WALLET_URL = cfg.hubWalletUrl;
|
|
VITE_HUB_CHAT_URL = cfg.hubChatUrl;
|
|
VITE_HUB_FORUM_URL = cfg.hubForumUrl;
|
|
VITE_HUB_MARKET_URL = cfg.hubMarketUrl;
|
|
VITE_HUB_TASKS_URL = cfg.hubTasksUrl;
|
|
VITE_HUB_RESTAURANT_URL = cfg.hubRestaurantUrl;
|
|
};
|
|
};
|
|
|
|
in {
|
|
options.services.webapp = {
|
|
enable = lib.mkEnableOption "AIO webapp";
|
|
|
|
flake = lib.mkOption {
|
|
type = lib.types.attrs;
|
|
example = lib.literalExpression "inputs.webapp";
|
|
description = ''
|
|
The webapp flake (as a flake input, not raw source). The module
|
|
calls `flake.lib.mkWebapp` to build. Pin per-host to pick the
|
|
right channel — `inputs.webapp` for main, `inputs.webapp-dev`
|
|
for the dev/staging channel.
|
|
'';
|
|
};
|
|
|
|
brandDir = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
example = lib.literalExpression "./../branding/cfaun";
|
|
description = ''
|
|
Path to the brand kit directory consumed by the build. When
|
|
null (default), the module synthesizes a kit from `appName` +
|
|
the flake's default logo — keeps existing hosts working
|
|
unchanged. Set this to a real brand kit when the host wants its
|
|
own logo + manifest theme. See `branding/README.md` in the
|
|
webapp repo.
|
|
'';
|
|
};
|
|
|
|
appName = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "AIO";
|
|
example = "Bouge";
|
|
description = ''
|
|
Application name shown in the PWA manifest, browser tab, and
|
|
home-screen install label. Used as the `brand.name` of a
|
|
synthesized brand kit when `brandDir` is null. When `brandDir`
|
|
is set, this option becomes informational only — brand.json
|
|
wins.
|
|
'';
|
|
};
|
|
|
|
domain = lib.mkOption {
|
|
type = lib.types.str;
|
|
example = "app.example.com";
|
|
description = "Domain name for the webapp";
|
|
};
|
|
|
|
# Nostr Configuration
|
|
nostrRelays = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ "wss://relay.damus.io" "wss://relay.snort.social" ];
|
|
description = "List of Nostr relay URLs";
|
|
};
|
|
|
|
adminPubkeys = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [];
|
|
description = "List of admin Nostr public keys (hex format)";
|
|
};
|
|
|
|
# API Configuration
|
|
lnbitsBaseUrl = lib.mkOption {
|
|
type = lib.types.str;
|
|
example = "https://lnbits.example.com";
|
|
description = "LNBits API base URL";
|
|
};
|
|
|
|
lnbitsDebug = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Enable LNBits debug mode";
|
|
};
|
|
|
|
lnbitsNostrTransportPubkey = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
example = "ac766168d4f3232772b16002a059cdf2ffed7293a25da620a2ddbdab7691c07f";
|
|
description = ''
|
|
Hex-encoded Nostr public key identifying the LNbits instance on
|
|
the relay network. The webapp uses this to address NIP-44 v2
|
|
encrypted kind-21000 RPC requests at the lnbits backend over
|
|
Nostr instead of HTTP. Empty string = nostr transport disabled.
|
|
'';
|
|
};
|
|
|
|
websocketEnabled = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Enable WebSocket for real-time updates";
|
|
};
|
|
|
|
# Lightning Address Domain
|
|
lightningDomain = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = ''
|
|
Domain used for Lightning Addresses.
|
|
Example: mydomain.com will show addresses as username@mydomain.com
|
|
'';
|
|
};
|
|
|
|
# Push Notifications
|
|
vapidPublicKey = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = "VAPID public key for push notifications";
|
|
};
|
|
|
|
pushNotificationsEnabled = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Enable push notifications";
|
|
};
|
|
|
|
# Image Upload Configuration (pict-rs)
|
|
pictrsBaseUrl = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = "Pict-rs image service base URL";
|
|
};
|
|
|
|
# Market Configuration
|
|
marketNaddr = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = "Nostr address (naddr) for the market configuration";
|
|
};
|
|
|
|
demoMode = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Enable demo mode with auto-generated demo accounts on the login page";
|
|
};
|
|
|
|
# Hub → standalone navigation URLs (one per chakra tile).
|
|
# Empty string = tile rendered ghosted (no link). Set per-host based on
|
|
# whether each standalone is deployed and on the deployment shape
|
|
# (path-mode → trailing slash recommended; subdomain-mode → no path).
|
|
hubEventsUrl = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
example = "https://demo.example.com/events/";
|
|
description = "URL the chakra hub's Events tile links to";
|
|
};
|
|
hubLibraUrl = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
example = "https://demo.example.com/libra/";
|
|
description = "URL the chakra hub's Libra tile links to";
|
|
};
|
|
hubWalletUrl = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
example = "https://demo.example.com/wallet/";
|
|
description = "URL the chakra hub's Wallet tile links to";
|
|
};
|
|
hubChatUrl = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
example = "https://demo.example.com/chat/";
|
|
description = "URL the chakra hub's Chat tile links to";
|
|
};
|
|
hubForumUrl = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
example = "https://demo.example.com/forum/";
|
|
description = "URL the chakra hub's Forum tile links to";
|
|
};
|
|
hubMarketUrl = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
example = "https://demo.example.com/market/";
|
|
description = "URL the chakra hub's Market tile links to";
|
|
};
|
|
hubTasksUrl = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
example = "https://demo.example.com/tasks/";
|
|
description = "URL the chakra hub's Tasks tile links to";
|
|
};
|
|
hubRestaurantUrl = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
example = "https://demo.example.com/restaurant/";
|
|
description = "URL the chakra hub's Restaurant tile links to";
|
|
};
|
|
|
|
enableSSL = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Enable SSL/TLS with Let's Encrypt";
|
|
};
|
|
|
|
acmeEmail = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = "Email for Let's Encrypt certificate notifications";
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
# lib.mkWebapp's output layout is `$out/dist/`, not `$out/share/webapp/`,
|
|
# so the vhost root points directly at dist/.
|
|
services.nginx.virtualHosts.${cfg.domain} = {
|
|
forceSSL = cfg.enableSSL;
|
|
enableACME = cfg.enableSSL;
|
|
|
|
root = "${webapp}/dist";
|
|
|
|
locations = {
|
|
"/" = {
|
|
# SPA shell must revalidate on every load so new deploys are
|
|
# picked up without the browser holding a stale shell pointing
|
|
# at deleted hashed assets. The more-specific regex location
|
|
# below overrides this for hashed static assets, which stay
|
|
# immutable.
|
|
tryFiles = "$uri $uri/ /index.html";
|
|
extraConfig = ''
|
|
add_header Cache-Control "no-cache";
|
|
'';
|
|
};
|
|
|
|
# Cache static assets aggressively (Vite emits content-hashed filenames)
|
|
"~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$" = {
|
|
extraConfig = ''
|
|
expires 1y;
|
|
add_header Cache-Control "public, immutable";
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
# ACME and firewall are configured globally in config/nginx.nix
|
|
};
|
|
}
|