Commit graph

1,004 commits

Author SHA1 Message Date
fadf5407a5 Merge pull request 'feat(branding): brand kit architecture (Phase 1)' (#96) from feat/brand-kit into dev
Reviewed-on: #96
2026-06-10 08:17:55 +00:00
be427f1821 feat(branding): swap default brand logo for proper AIO mark
Replaces the seed copy of src/assets/logo.png (8-bit colormap) with
the official AIO logo from ~/Pictures/AIO/aio.png (8-bit/color RGBA,
1024x1024, with alpha — better for maskable icons).

Generator output verified: all 6 icons regenerate cleanly from the
new source.

Part of aiolabs/webapp#95 / #96.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 09:51:28 +02:00
3dfed23b43 docs(branding): brand kit contract + CLAUDE.md section
branding/README.md is the deployer contract:
- Directory layout, source format constraints (SVG > PNG ≥ 1024),
  brand.json schema, per-standalone override resolution order
- BRAND_DIR / BRAND_APP usage, generator pipeline walkthrough
- Pointer to issue #95 + the NixOS Phase 2 integration

webapp CLAUDE.md gains a Brand Kit section describing the moving
parts (vite-branding.ts, @brand alias, brandAssetsPlugin,
public/icons/ gitignore, per-app override path) so future sessions
on this repo know the convention without grepping.

Adds BRAND_DIR / BRAND_APP to the Environment Variables example.

Workspace ~/dev/CLAUDE.md note about "brand changes don't need
flake.lock bump" deferred to Phase 2 (server-deploy migration) —
that's when the workflow becomes reality.

Part of aiolabs/webapp#95.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 00:02:23 +02:00
3efae30e84 feat(branding): auto-generate icons on vite build/dev start
vite-branding.ts now exports brandAssetsPlugin() — a Vite plugin
whose buildStart hook runs scripts/generate-pwa-assets.mjs once per
build / dev-server start. All 9 vite configs register it first in
plugins[], so PWA icons under public/icons/ are guaranteed to exist
before VitePWA's includeAssets / manifest.icons get processed and
before the public/ → dist/ copy.

Removes the "did you remember to pnpm generate-pwa-assets?" failure
mode. Dev mode now also auto-populates icons (no more dev 404s on
/icons/favicon.ico).

Verified build from clean state (no public/icons/ pre-existing): the
plugin generates, all 6 icons land in dist-wallet/icons/.

Part of aiolabs/webapp#95.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 23:43:50 +02:00
faf41cd1c0 refactor(branding): switch to /icons/ paths and remove committed binaries
PWA icons now ship from public/icons/ (generated by
@vite-pwa/assets-generator, gitignored). The seven hand-crafted
binaries at public/ root come out of the tree.

Changes:
- 7 deleted: public/{favicon.ico, apple-touch-icon.png, mask-icon.svg,
  icon-{192,512}.png, icon-maskable-{192,512}.png}
- 9 HTML: <link rel="icon"|"apple-touch-icon"> hrefs prefixed with
  /icons/. mask-icon link dropped (PNG source → no sharp SVG; modern
  browsers prefer favicon.svg anyway, which we can revisit when an
  SVG brand source lands).
- 8 vite configs: includeAssets[] + manifest.icons[].src prefixed
  with icons/. Vite rewrites /icons/foo → <base>/icons/foo when base
  is set (so /events/icons/favicon.ico under /events/ deploys).

Build is now dependent on `pnpm generate-pwa-assets` running first
(or whatever invokes the generator — Phase 2 NixOS builds wire this
into buildNpmPackage). Standalone dev runs the generator on first
boot or whenever BRAND_DIR changes.

Part of aiolabs/webapp#95.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 23:37:55 +02:00
4e7502b30c refactor(branding): drop VITE_APP_NAME compat shim
Per the pre-public-launch policy in CLAUDE.md, strict-from-the-start.
brand.json is the sole source for app naming; VITE_APP_NAME no longer
overrides.

Migration responsibility moves to aiolabs/server-deploy#8 (Phase 2):
hosts that currently set VITE_APP_NAME (cfaun → "Bouge") must instead
declare BRAND_DIR pointing at a per-host branding/ directory before
their next webapp flake input bump.

process.env.VITE_APP_NAME is still set internally (from brand.name) to
keep Vite's HTML %VITE_APP_NAME% substitution working — but it's
internal wiring now, not a user-facing knob.

Part of aiolabs/webapp#95.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 23:15:58 +02:00
ce5a1a6a56 feat(branding): drive PWA manifest from brand.json
vite-branding.ts now loads brand.json into a typed `brand` object and
exports a `brandManifestName()` helper. Schema:

  { name (required), shortName?, themeColor?, backgroundColor? }

Default brand.json drops themeColor/backgroundColor — they're optional
overrides; per-app accents (wallet yellow, chat green, …) keep working
via `?? '<existing>'` fallbacks in each standalone's vite config.

events: manifest.name/short_name driven by brand. VITE_APP_NAME env
override stays (Phase 2 server-deploy migration still in flight) and,
when set, overrides both name and short_name to preserve pre-#95
behavior. cfaun's VITE_APP_NAME=Bouge keeps working unchanged.

hub (vite.config.ts): brand.name flows into %VITE_APP_NAME% Hub title.

7 other standalones (wallet, chat, market, forum, tasks, restaurant,
libra): only theme_color/background_color get brand overrides. Their
manifest.name/short_name stay hardcoded so multi-PWA home-screen
labels remain differentiated ("Wallet", "Chat", …) rather than all
collapsing to the brand short_name.

Verified default build: events manifest name=AIO; wallet keeps
"Wallet — Lightning" + #eab308 accent.
Verified VITE_APP_NAME=Sortir override: events name+short_name=Sortir.

Part of aiolabs/webapp#95.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 23:11:30 +02:00
88ab432629 fix(branding): wrap generator to clean up staged brand source
pwa-assets.config.ts stages the brand logo in public/icons/.brand-source.*
so the CLI (which emits next to its source) writes alongside it. Without
cleanup, the full-resolution 1024+ source ends up in dist/icons/ on every
build and is publicly served at /icons/.brand-source.png.

scripts/generate-pwa-assets.mjs runs the CLI, then removes the staged
source. Wire it through the `generate-pwa-assets` pnpm script.

Part of aiolabs/webapp#95.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 22:58:07 +02:00
eebb566323 feat(branding): add @brand vite alias + migrate in-app img consumers
vite-branding.ts is the shared resolver. Exports BRAND_DIR (absolute,
defaults to ./branding/default) and brandAlias for spreading into each
vite config's resolve.alias map.

All 9 vite configs now spread brandAlias so `@brand/<file>` resolves
to the active brand dir at build time.

Migrates the four <img src="@/assets/logo.png"> consumers
(Login.vue, LoginDemo.vue, AppSidebar.vue, MobileDrawer.vue) to
@brand/logo.png. Unused Navbar.old.vue left as-is.

Build verified: dist/assets/logo-<hash>.png emits from the aliased
import. Future deployers point BRAND_DIR at their brand kit and the
in-app logo follows automatically.

Part of aiolabs/webapp#95.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 22:55:42 +02:00
50a345ce4e feat(branding): install @vite-pwa/assets-generator + config
Adds pwa-assets.config.ts that reads $BRAND_DIR (default
./branding/default) and $BRAND_APP (optional per-standalone
override), resolves logo.svg/logo.png with documented fallback
order, and emits the existing icon set (favicon.ico,
icon-{192,512}.png, icon-maskable-{192,512}.png,
apple-touch-icon.png) into public/icons/.

Generator outputs alongside its source, so the config stages the
brand source into public/icons/.brand-source.{svg,png}; gitignoring
public/icons/ covers both staged source and generated icons in one
line.

Adds pnpm script `generate-pwa-assets`. Vite configs / HTML <link>
href updates come in follow-up commits; this commit alone produces
the icon set under public/icons/ but doesn't yet replace the
committed public/*.png binaries.

Part of aiolabs/webapp#95 (brand kit architecture).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 22:38:26 +02:00
a8c997ca8d feat(branding): scaffold default brand kit
Introduces branding/default/ as the unparameterized aiolabs brand:
- logo.png (1024x1024, seeded from src/assets/logo.png)
- brand.json with name, shortName, themeColor, backgroundColor

First step toward white-label PWA branding (aiolabs/webapp#95). No
consumer wiring yet — that's the next commits. Future deployers
(NixOS hosts in server-deploy, third-party white-labelers) point
BRAND_DIR at their own variant of this layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 21:59:13 +02:00
5541d2bc7a refactor(events): rename activities module to events + wire VITE_APP_NAME for per-deployment branding (#94)
## Why

The module was named `activities` originally to avoid colliding with Nostr's `Event` type. In practice that defense added friction without preventing confusion — the backend extension is named `events`, NIP-52 calls them "Calendar Events", and the UI already displayed "Events". The collision with `nostr-tools` `Event` is handled cleanly by the existing `import { Event as NostrEvent }` alias pattern (already in 5 files inside the module). Renaming collapses the 4-way mismatch into one consistent term.

Separately, deployments needed per-instance app names. `VITE_APP_NAME` was already plumbed per-standalone via NixOS `services.webapp-standalones.<app>.displayName` (e.g. cfaun shipped `"Sortir"`, now rebranded to `"Bouge"`), but nothing in the standalone consumed it — PWA manifest, HTML title, and runtime branding were all hardcoded. This PR wires the env through every app-name display point so any deploy can flip `displayName = "Bouge"` (or anything) and pick up the brand end-to-end.

## What

Nine commits on the branch:

1. **`refactor(events): rename activities module to events`** — 70-file rename. `src/modules/activities/` → `src/modules/events/`, `src/activities-app/` → `src/events-app/`, types/services/composables/views/components/store renamed (`Activity`→`Event`, `Activities`→`Events`). Routes `/activities/*` → `/events/*`; the legacy `/events` (ticketing management) moves to `/my-events` so `/events` belongs to the canonical feed. DI tokens `ACTIVITIES_*` → `EVENTS_*`. i18n namespace renamed; English domain strings updated, French/Spanish title key realigned. npm scripts `:activities` → `:events`. Build output `dist-activities/` → `dist-events/`.

2. **`feat(events): wire VITE_APP_NAME through PWA manifest, HTML, runtime`** — PWA manifest `name`/`short_name` template from `process.env.VITE_APP_NAME` with fallback `'Events'`. `events.html` uses Vite's `%VITE_APP_NAME%` substitution. `src/events-app/app.ts` + `main.ts` runtime brand string drives console logs, offline notification, and `acceptTokenFromUrl()`. `events.title` route meta sources from VITE_APP_NAME. `.env.example` updated with per-standalone scoping notes.

3. **`docs(events): update activities→events references`** — `docs/nostr-patterns/*.md` "Canonical: src/modules/activities/composables/useRSVP.ts" anchors point at renamed paths. CLAUDE.md Payment Rails Pattern section updated.

4. **`fix(events): drop lowercase from PWA description brand name`** — minor casing fix caught during verification.

5. **`fix(events): use domain noun in description, not brand name`** — manifest + HTML description switched from `"Discover ${BRAND} near you"` to static `"Discover events near you"`. Description is about *what* the app does, not the brand.

6. **`chore(events): scrub leftover sortir/activities references`** — nginx.conf.example, package.json (concurrently process label + `build:demo` BASE_PATH), router-helpers comment, useMarket comment, and vite.events.config.ts doc-comment.

7. **`refactor(events): conditional brand in console label, tighten docs`** — adds `APP_LABEL` next to `APP_NAME` in `events-app/app.ts`. Reads as `Events` on unbranded builds and `Events (Bouge)` (etc.) when `VITE_APP_NAME` is set to anything other than "Events" (case-insensitive). Used by all four console messages; `acceptTokenFromUrl` keeps the raw `APP_NAME` (it's a token-namespace identifier, not display copy). Also collapses the redundant "cfaun sets X; future deployments can override (e.g. X)" doc-comment.

8. **`i18n(events): finish activité/actividad → événement/evento sweep`** — completes the i18n rename in fr.ts and es.ts (search placeholders, favorites prompts, settings prompt) and fixes five gender-agreement errors that came along with the masculine `événement`/`evento` switch (French: `Aucune ... trouvée`→`Aucun ... trouvé`, `préférées`→`préférés`, `d'une ... la sauvegarder`→`d'un ... le sauvegarder`; Spanish: `favoritas`→`favoritos`, `guardarla`→`guardarlo`).

9. **`chore(events): finish sortir → bouge sweep in .env.example`** — four remaining doc-comment refs in `.env.example` (cfaun branding, ticket-scanner comment, section header, subdomain-mode URL example).

## Cross-repo coordination

This PR has matching changes already pushed to two other repos. They have to land in a coordinated bump because the names are tightly coupled.

- **`aiolabs/webapp-module` main** — commit `9d82016`. Renames `hubActivitiesUrl` → `hubEventsUrl` and `VITE_HUB_ACTIVITIES_URL` → `VITE_HUB_EVENTS_URL`. No backwards-compat shim.
- **`aiolabs/server-deploy` main** — commits `f15e1eb`, `bf4698b`, `d46a520`:
  - `standalones.nix` `apps.events` uses `build:events` / `dist-events` / `events.html`; `hubUrlOption` maps `events → "hubEventsUrl"`; comment + cfaun-as-example doc switched from sortir/Sortir to bouge/Bouge.
  - `hosts/cfaun/default.nix` rebranded: `subdomain = "bouge"` + `displayName = "Bouge"`. (Native-French feedback retired the "Sortir" branding as awkward.)
  - `hosts/atio/default.nix` stale "activities app" comment dropped (atio's `displayName = "Eventos"` config is unchanged).

server-deploy's `flake.lock` still pins the OLD webapp + OLD webapp-module, so nix builds from server-deploy main will fail until the bumps. Suggested order after this PR merges to `dev`: one server-deploy commit that does `nix flake lock --update-input webapp-module` + `--update-input webapp-demo` together. DNS for `bouge.ariege.io` needs to point at the cfaun host before the deploy lands.

## Verification

- `pnpm typecheck` — clean ✓
- `pnpm build:events` (default) — `dist-events/manifest.webmanifest` has `name: "Events"`, description "Discover events near you" ✓
- `VITE_APP_NAME=Bouge pnpm build:events` — `name: "Bouge"`, HTML title "Bouge", console label resolves to `Events (Bouge)` ✓
- Cross-repo grep sweep: no `Sortir`/`sortir`/`activities`/`Activities` references in events-module scope. (Domain false positives — "Market Activity" in market dashboard, "time-based activities" in nostr-feed content filters, "Activity Lifecycle Kills" in CLAUDE.md mobile-browser docs — are unrelated and intentionally left alone.)

Not verified in-browser (no GUI in this session) — needs a manual smoke at `app.ariege.io/bouge/` once the coordinated flake bump lands on cfaun. Watch for: feed loads, `/events/calendar`, `/events/map`, `/events/favorites`, `/events/:id` routes work; "My Events" appears in the user dropdown and `/my-events` shows the management page; PWA install prompt shows "Bouge".

## Out of scope (deferred)

- Backend extension rename — `aiolabs/events` is already named correctly.
- Nix standalone attribute name — `services.webapp-standalones.events` is already correct.
- Overriding domain-noun strings ("Your event was created", RSVP labels) — those stay locale-driven; `VITE_APP_NAME` covers app-name only.
- Backwards-compat redirects from `/activities/*` to `/events/*` — pre-launch, public URL was `/sortir/` (path-mode) or `sortir.ariege.io` (subdomain-mode), never `/activities/`. No external bookmarks to preserve. (`sortir.ariege.io` → `bouge.ariege.io` is a separate decision; up to the deploy operator whether to keep a 301 for word-of-mouth referrers.)

Reviewed-on: #94
2026-06-09 18:18:26 +00:00
1f20d5f00c Merge pull request 'refactor(libra): redesign transactions list status + type encoding' (#93) from feat/libra-tx-status-encoding into dev
Reviewed-on: #93
2026-06-06 21:16:45 +00:00
75dfd8a541 refactor(libra): redesign transactions list status + type encoding
Rework how the standalone transactions list communicates entry status
and type so each visual channel does one job and the filter UI matches
the underlying axes.

Encoding:
- Type lives in the signed/colored amount (+green income, -red expense)
  and a matching Income/Expense badge in the badge row.
- Status lives in badges only: red Voided (leftmost) and yellow Pending
  (after the type badge). Cleared entries carry no status badge — the
  quiet default.
- Voided rows additionally strike-through and mute the amount.
- Drop the title-row status icons and the colored left border that
  previously fought with the amount color for the same meaning.

Filter UI:
- Replace the type radio + voided switch with three category chips —
  Income, Expenses, Voided — that independently toggle inclusion of one
  bucket of rows. Each row belongs to exactly one bucket (voided wins
  over type). Defaults: Income + Expenses on, Voided off.
- Empty-selection state nudges the user to enable a category.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 23:12:57 +02:00
4af220adda Merge pull request 'feat(libra): show voided transactions in standalone' (#92) from feat/libra-show-voided into dev
Reviewed-on: #92
2026-06-06 20:31:58 +00:00
1fbf7b3d26 fix(libra): exclude voided txs from balance Pending section
BalancePage filtered tx.flag === '!' to compute pending count/sum/list.
After the libra backend stops hiding voided transactions, those will
arrive with flag='!' plus a 'voided' tag and would otherwise leak into
the Pending section. Add the tag-aware exclusion to keep Pending
showing only genuinely pending entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 20:45:55 +02:00
e9195978c1 feat(libra): surface voided transactions in standalone history
Voided entries keep their '!' flag and gain a 'voided' tag per the libra
reject convention, so detecting them needs a tag check rather than a new
flag char. Render them inline with the existing 'x'-flag voided styling
(grey XCircle icon, strike-through title/amount, red-tinted Voided badge)
so users like Nancy can see their rejected entries instead of having them
silently disappear from the list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 20:45:25 +02:00
4c704e5a41 chore(expenses): delete orphaned admin permission UI
PermissionManager.vue and GrantPermissionDialog.vue were never
imported or routed anywhere; the three ExpensesAPI methods backing
them (listPermissions, grantPermission, revokePermission) pointed
at /libra/api/v1/permissions* which doesn't exist on the backend
(real path is /api/v1/admin/permissions*). The whole feature has
been unreachable since whenever the path drifted.

Removes the two components, the three API methods, and the four
types only they used (AccountPermission, GrantPermissionRequest,
AccountWithPermissions, PermissionType).

If cross-account permission management becomes a real need, the
backend at aiolabs/libra already provides the endpoints (now
correctly gated by require_super_user); rebuild the UI fresh
against the right paths rather than reviving this dead surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 23:17:52 +02:00
ce2488941f Merge pull request 'feat(webapp): add color scheme switcher with 7 palettes' (#90) from feat/color-scheme-switcher into dev
Reviewed-on: #90
2026-06-04 09:51:43 +00:00
53af36ad01 feat(webapp): add color scheme switcher with 7 palettes
Replace the bespoke index.css with a shadcn-vue-idiomatic theme.css
(Catppuccin Latte/Mocha as the default), and add a palette picker to the
profile sheet that swaps between 6 alternative palettes scoped under
:root[data-theme="<name>"]: Countryside Castle, Dark Matter, Emerald
Forest, Light Green, Neo Brutalist, Starry Night.

useTheme() now also persists a 'ui-palette' localStorage key alongside
the existing 'ui-theme' (dark/light/system) and applies the choice via a
data-theme attribute on <html>. Standalone apps inherit the palette
automatically since AppShell already invokes useTheme() on mount.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 11:50:19 +02:00
ca3ad434d3 Merge pull request 'fix(activities): surface statsError on the door-scanner page' (#89) from fix/scanner-stats-error-banner into dev
Reviewed-on: #89
2026-06-04 09:49:39 +00:00
b8910868cd fix(activities): surface statsError on the door-scanner page
useTicketScanner already captures the stats fetch error into a ref
but ScanTicketsPage never read it, so a 404 / 403 / auth failure on
GET /tickets/event/{id}/stats was completely silent — the counts
strip kept showing the last good value while scans landed on the
backend, making it look like the scanner was broken when actually
the refresh path was just dead.

Adds a small destructive-toned banner under the counts strip,
visible across both Scanner and Scanned tabs. AlertCircle already
imported. No new composable surface — statsError is already exported
from useTicketScanner.

Surfaced by a missing /stats endpoint on aio-demo's events backend
(now shipped as events 1.6.1-aio.5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 19:53:48 +02:00
52c03328b4 Merge pull request 'feat(base): phase-2 bucket-B migration via signEventViaLnbits' (#88) from feat/phase-2-bucket-b-sign-event into dev
Reviewed-on: #88
2026-06-03 16:50:13 +00:00
ebd8cef8cd feat(base): phase-2 bucket-B migration via signEventViaLnbits
Closes the build-fail interval opened by PR #84 (User.prvkey field
removal). Adds the uniform signEventViaLnbits() helper per
design-questions Q3.3 and migrates the 5 compile-failing sites the
prvkey removal exposed.

New helper at src/lib/nostr/signing.ts:
- POST /api/v1/auth/sign-event with Bearer auth + credentials:include
- Lazy CSRF token fetch + cache; one-shot refresh on 403-with-CSRF
- Returns the fully-signed event for caller to publish

Site migrations:
- activities/composables/useBookmarks.ts (kind 10003) — drop finalizeEvent
- activities/composables/useRSVP.ts (kind 31925) — drop finalizeEvent
- nostr-feed/components/NostrFeed.vue (kind 5 deletion) — drop finalizeEvent
- base/services/NostrTransportService.ts (kind 21000 RPC bootstrap) —
  call() now throws "deferred to phase 3+ per Q4.2/Q4.3"; scaffolding
  retained for the eventual transport revival
- market/composables/useMarket.ts (NIP-59 gift-wrap unwrap) — disabled
  with a console.warn; no server-routed nip44_decrypt endpoint exists
  yet (Bucket C territory)

Known regression intentionally accepted: incoming order-DM gift-wrap
processing in the marketplace is non-functional until phase-3 adds
NIP-44 decrypt over HTTP/bunker. Per design-questions §"Open questions
deferred", marketplace order receipt routes through nostrmarket
server-side anyway; this client-side path was a redundant fast-path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 18:48:42 +02:00
9048248353 Merge pull request 'fix(activities): route ticket scanner through HTTP, not nostr-transport RPC' (#87) from fix/scanner-via-http into dev
Reviewed-on: #87
2026-06-03 16:34:01 +00:00
ce4ee80359 fix(activities): route ticket scanner through HTTP, not nostr-transport RPC
Post-aiolabs/lnbits#9 the webapp no longer holds a raw user prvkey,
so the kind-21000 nostr-transport RPC layer fails closed for every
caller at the "Sign-in with a Nostr key required to call RPC" guard
in NostrTransportService.call. The ticket scanner was the only
remaining user of that transport on the organizer side.

Route the door scanner through the events extension's existing
admin_key-gated HTTP endpoints instead, matching the Bucket A
pattern the team converged on for the rest of the prvkey-removal
migration (operator-class events route through extension HTTP,
not webapp-side signing).

Pairs with a new GET /tickets/event/{id}/stats endpoint on the
events extension (admin_key + owner check, mirroring the
events_list_event_tickets RPC shape). PUT /tickets/register/{id}
was already hardened in v1.6.1-aio.3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 16:34:01 +00:00
386273baab Merge pull request 'chore(api): remove User.prvkey field + thread-through helpers (Q1.2 Option b)' (#84) from chore/remove-user-prvkey-field into dev
Reviewed-on: #84
2026-06-03 16:33:48 +00:00
c07de62af1 fix(nostr-feed): repoint dangling ScheduledEventService imports to TaskService
The dedup commit (e2a1f02) deleted nostr-feed/services/ScheduledEventService.ts
but missed two type imports in NostrFeed.vue and ScheduledEventCard.vue.
vue-tsc failed in the chat-app standalone build with TS2307.

The ScheduledEvent / EventCompletion / TaskStatus types now live in
@/modules/tasks/services/TaskService (already where useTasks comes from
post-dedup).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 12:13:36 +02:00
9a300c1679 chore(api): remove User.prvkey field + thread-through helpers (Q1.2 Option b)
Atomic phase-1 final per design-questions Q1.2 Option (b) and the
2026-05-29T00:30Z architecture-decisions lock-in. Removing the
prvkey?: string field from the User interface flips the type system
into the pressure mechanism that forces phase-2 to start: every
remaining bucket-B sign-site (chat / forum / nostr-feed /
activities-bookmarks/RSVP / market / tasks) now fails vue-tsc until
it migrates to signEventViaLnbits() against POST /api/v1/auth/sign-event
(aiolabs/lnbits PR #29, deployed on aio-demo).

Changes:
- src/lib/api/lnbits.ts:
  - Drop `prvkey?: string` from User interface.
  - getCurrentUser(): /auth/nostr/me used to merge prvkey alongside
    pubkey; post-cascade the endpoint returns only the pubkey.
    Updated the comment + cleaned the merge object.
- src/modules/base/auth/auth-service.ts:
  - updateProfile() no longer threads `prvkey` through the merge.
    Server-side PATCH /auth publishes kind-0 via the signer per
    869f67c3; the webapp doesn't keep prvkey at all.
- src/modules/nostr-feed/components/NostrFeed.vue +
  src/modules/nostr-feed/components/ScheduledEventCard.vue:
  - Repoint the `ScheduledEvent` type import from the deleted
    `../services/ScheduledEventService` to
    `@/modules/tasks/services/TaskService`. Trivial post-#81-merge
    cleanup that fell through the dedup PR; same file exports the
    same interface.

vue-tsc --noEmit fails with 8 errors after this commit, all
TS2339 "Property 'prvkey' does not exist". The failing sites are
exactly the bucket-B targets the design doc enumerates as
phase-2 migration work:

| Failing site                                  | Bucket B kind |
|-----------------------------------------------|---------------|
| activities/composables/useBookmarks.ts:92,113 | kind 10003 (NIP-51 bookmarks) |
| activities/composables/useRSVP.ts:153,187     | kind 31925 (NIP-52 RSVP) |
| base/services/NostrTransportService.ts:100,112| kind 21000 (NIP-44 v2 RPC envelope) |
| market/composables/useMarket.ts:455           | NIP-44 gift-wrap (kind 1059) unwrap |
| nostr-feed/components/NostrFeed.vue:408       | kind 5 (deletion of own post) |

NOT caught by vue-tsc but still bucket-B (BaseService injection
pattern types `this.authService` as `any`, so optional chaining
bypasses the type check):

- chat/services/chat-service.ts:341,511,714
- forum/services/SubmissionService.ts:755,1167
- nostr-feed/services/SubmissionService.ts:769,1226
- nostr-feed/components/NoteComposer.vue:306
- nostr-feed/components/RideshareComposer.vue:423
- tasks/services/TaskService.ts:507,562,616

Those sites will runtime-fail (prvkey is undefined from the API
post-cascade) but won't surface at compile time. Phase 2's
per-module migration (Q5.2) catches them as each module flips.

The webapp WILL NOT BUILD CLEANLY after this PR merges to dev
until phase 2 lands. That's the intended trade-off per Q5.1 +
Q1.2; the broken-build interval is the design-intended pressure
mechanism to start phase 2. server-deploy's webapp-demo flake.lock
bump will fail until phase 2 lands; demo will stay on the
pre-PR-#84 webapp during that interval.

Refs:
- log:2026-05-29T00:30Z (consolidated decisions; Q1.2 Option (b)
  + Q5.1 risk: demo gap acceptable)
- log:2026-05-29T17:30Z (lnbits confirming this PR stays
  atomic-after-the-two-bucket-A PRs)
- ~/dev/coordination/webapp-design-questions.md Q1.2 + Q5.1
- Parent initiative: aiolabs/lnbits#9 (signer abstraction / bunker)
- Sibling PRs (stacked base→head): #82#83 → this

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 17:29:45 +02:00
05bbe68682 Merge pull request 'chore(activities): reroute CreateActivityDialog through TicketApiService.createEvent' (#83) from chore/delete-activities-nostr-service-publish into dev
Reviewed-on: #83
2026-05-30 15:26:06 +00:00
9bef2d58ac chore(activities): reroute CreateActivityDialog through TicketApiService.createEvent
The aiolabs/events extension on its signer-abstraction branch (commit
66076d6) constructs and publishes kind-31922 NIP-52 calendar events
server-side via NostrSigner — POST /events/api/v1/events accepts a
CreateEventRequest payload, signs through the operator's signer, and
broadcasts to configured relays. The webapp no longer needs to sign
calendar events client-side.

Changes:
- ActivitiesNostrService.ts: delete publishCalendarEvent() and its
  helper imports (finalizeEvent, EventTemplate, buildCalendarTimeEventTags,
  the local hexToUint8Array). The subscribe / query paths stay — the
  service still reads NIP-52 events off relays for the activity feed.
  Docstring updated to reflect the read-only role and point at the
  events extension for the publish path.
- CreateActivityDialog.vue: swap the publish flow.
  - Drop ActivitiesNostrService injection + currentUser.value.prvkey read.
  - Inject TicketApiService instead; pull invoiceKey from
    currentUser.value.wallets[0].inkey (same pattern as EventsPage.vue
    handleCreateEvent).
  - Build CreateEventRequest with amount_tickets: 0, price_per_ticket: 0
    (events extension treats 0 as unlimited/not-ticketed per
    models.py:45-46 per lnbits 22:30Z audit).
  - Fold summary + description into the events extension's `info`
    field since CreateEventRequest has no separate summary slot.
  - Update toast on success to "Activity created!" (server publishes
    to relays via the signer, not the webapp).

Approval-workflow caveat documented inline in the submit handler:
non-admin users on instances with auto_approve=false (the default)
land in the proposal queue and don't publish to relays until an admin
approves. Admins / auto_approve=true instances publish immediately.
This is the intended new behavior — operators can flip auto_approve
on the events extension config per-instance if they want the legacy
direct-publish moderation posture.

This is webapp's second bucket-A leg per aiolabs/lnbits#9 phase-1.
The remaining `currentUser.value.prvkey` reads stay until the
atomic User.prvkey field-removal PR (Q1.2 Option (b)).

Refs:
- log:2026-05-28T22:30Z (lnbits Q2.1 audit verifying ticket-less
  acceptance + approval-workflow caveat)
- ~/dev/coordination/webapp-design-questions.md Q2.1
- aiolabs/events signer-abstraction commit 66076d6 (the server-side
  publish path)
- aiolabs/lnbits cascade tip 861f427c deployed to aio-demo

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 15:26:06 +00:00
bc565ebf4b Merge pull request 'chore(base): delete nostr-metadata-service + retire webapp-side kind-0 broadcast paths' (#82) from chore/delete-nostr-metadata-service into dev
Reviewed-on: #82
2026-05-30 15:25:47 +00:00
cb6e1351fb fix(activities): scope detail-page query by NIP-52 d-tag
`useActivityDetail.load()` previously asked every relay for every
kind-31922/31923 event and raced a 5s timeout to find the one
matching the route param. On a cold refresh of the detail page, the
race was often lost — the store starts empty (no feed subscription
to populate it), the relay sprays the whole calendar, and the
matching event may arrive after the timeout, leaving the user with
"Activity not found" on a valid URL.

Add a `dTags` field to `CalendarEventFilters` and emit it as the
nostr `#d` tag filter. Detail-page subscribe + query both scope to
the single activity, so the relay resolves a parameterized-replaceable
lookup in milliseconds instead of streaming the whole calendar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 08:03:55 +02:00
261eded316 fix(api): align webapp client with post-cascade lnbits + surface error detail
Two fixes that together make the kind-0 server-side publish path (this
PR's whole reason for existing) actually work end-to-end against the
deployed cascade:

1. **updateProfile() uses PATCH /api/v1/auth, not PUT /auth/update.**
   aiolabs/lnbits PR #26 gap-fill (869f67c3) wired
   _publish_nostr_metadata_event into the PATCH handler at
   auth_api.py:546. The legacy `/auth/update` route doesn't exist on
   the post-cascade server — a `PUT /auth/update` request gets routed
   into the `/auth/{provider}` SSO wildcard which only allows GET and
   returns 405. Caught while smoke-testing this PR against a local
   regtest pointed at the issue-18-phase-2.3 branch.

2. **request<T>() parses FastAPI's `{"detail": "..."}` error shape.**
   The old error path threw `API request failed: 405 Method Not Allowed`
   for the regtest's 405 above — useful only if you also opened the
   network panel and read the response body manually. Now we parse the
   detail (string or pydantic-validation array), include the endpoint
   path, and throw `LNbits /auth 405: Method Not Allowed`. Falls back
   to raw text for non-JSON bodies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 07:45:35 +02:00
414b79565c chore(base): delete nostr-metadata-service + retire webapp-side kind-0 broadcast paths
Lnbits's cascade now publishes kind-0 user metadata server-side on
account creation AND on every PATCH /api/v1/auth (aiolabs/lnbits commit
869f67c3 folded into PR #26, deployed to aio-demo via server-deploy
e2eed9c). The webapp no longer needs its own kind-0 publish surface.

Changes:
- Delete src/modules/base/nostr/nostr-metadata-service.ts (162 lines).
  Server now owns kind-0 lifecycle via NostrSigner.sign_event.
- Delete src/modules/base/composables/useNostrMetadata.ts (had zero
  callers; was just a thin wrapper around the deleted service).
- Remove NOSTR_METADATA_SERVICE token from di-container.ts.
- Remove all NostrMetadataService imports / instantiations /
  registrations / dispose calls from src/modules/base/index.ts.
- src/modules/base/auth/auth-service.ts:
  - Drop the broadcastNostrMetadata() helper entirely.
  - Drop its callers in login() (was line 118 pre-edit) and register()
    (was line 142 pre-edit) — both flagged for removal by lnbits in the
    01:45Z coordination handoff. Login-time republish was always
    redundant for kind-0 (replaceable event); register-time is covered
    by lnbits's create_user_account -> _publish_nostr_metadata_event
    path.
  - Drop the auto-broadcast in updateProfile() too — covered by the
    PATCH /api/v1/auth handler's _publish_nostr_metadata_event call
    per the gap-fill commit.
  - Leave the prvkey/pubkey preservation in updateProfile() in place
    for now; the prvkey field removal is the atomic phase-1 final PR
    per design doc Q1.2 Option (b).
- src/modules/base/components/ProfileSettings.vue:
  - Remove the "Broadcast to Nostr" button + isBroadcasting state +
    Radio icon + broadcastMetadata() handler. Manual re-broadcast was
    a local-testing safety net for relay resets that's no longer
    needed once the server publishes automatically on profile save.
  - Simplify the post-save toast to a generic "Profile updated!".
  - Update the helper text accordingly.

This is webapp's bucket-A leg per aiolabs/lnbits#9 phase-1 plan.

Refs:
- log:2026-05-29T01:45Z (lnbits handoff identifying the auth-service
  line numbers to drop)
- ~/dev/coordination/webapp-design-questions.md Q2.3 (decision context)
- aiolabs/lnbits PR #26 commit 869f67c3 (server-side kind-0 publish)
- aiolabs/lnbits dev tip 861f427c, deployed to aio-demo

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 21:38:01 +02:00
114d2837c9 Merge pull request 'chore(nostr-feed): delete legacy ScheduledEventService duplicate' (#81) from chore/dedup-scheduled-event-service into dev
Reviewed-on: #81
2026-05-29 19:33:40 +00:00
221c927c74 Merge pull request 'chore(nostr-feed): delete dead-code ReactionService + useReactions duplicates' (#80) from chore/dedup-reaction-service into dev
Reviewed-on: #80
2026-05-29 19:33:27 +00:00
e2a1f024e4 chore(nostr-feed): delete legacy ScheduledEventService duplicate
ScheduledEventService and useScheduledEvents were a legacy duplicate
of TaskService and useTasks. The DI token was already marked
@deprecated, and FeedService routed runtime events to TASK_SERVICE
already — only the publish-side and the NostrFeed view hadn't been
repointed yet.

Changes:
- Delete nostr-feed/services/ScheduledEventService.ts (1067 lines)
- Delete nostr-feed/composables/useScheduledEvents.ts
- Remove SCHEDULED_EVENT_SERVICE token from di-container.ts
- Repoint NostrFeed.vue to useTasks from the tasks module, with
  autoSubscribe: false (FeedService still owns the relay subscription
  for kinds 31922/31925/5)
- Rename FeedService.scheduledEventService field to taskService for
  honesty (the alias was already pointing at TASK_SERVICE)
- Drop tryInjectService legacy-fallback shim — strict-from-the-start
  per workspace pre-launch policy. The tasks module is required;
  inject hard-fails on absence.
- Remove now-dead defensive null guards around taskService and
  reactionService calls in route methods

Closes #79.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:12:21 +02:00
99ca0bf64a chore(nostr-feed): delete dead-code ReactionService + useReactions duplicates
The nostr-feed module had its own copies of ReactionService and
useReactions that were never wired in — the live implementations
live in src/modules/base/. The nostr-feed copy of ReactionService
was a strict subset of the base copy (missing toggleLikeEvent /
toggleDislikeEvent) and was never registered in DI. The
nostr-feed copy of useReactions was identical to the base copy
modulo the type import path; the one consumer (NostrFeed.vue)
already imports from the base path.

Closes #78.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 16:51:47 +02:00
464ee642de ui(qr-scanner): swap flash-toggle icon from lightning-bolt to flashlight
The control toggles the camera torch, but the inline-SVG bolt read
as a Lightning Network glyph — confusing on a Lightning-payments
app, especially next to the scanner used to validate paid tickets.
Use lucide's Flashlight icon for an unambiguous match to the
control's actual function, and add an aria-label while we're here.
2026-05-25 12:07:20 +02:00
aee29f1ad5 fix(activities): favorites tab shows login toast instead of navigating when logged out
Tapping Favorites used to drop the user onto an empty page that
*then* fired the auth-prompt toast on mount — the navigation was
wasted motion. Mirror the Create tab pattern: onClick checks auth
first, fires the same toast (with Log in action) when unauthed,
and only router-pushes when authed.

`path` is kept on the tab so the active-highlight still works when
the user is actually on /activities/favorites after logging in
(BottomNav highlights via tab.path && isActive(tab.path)). The
mount-time toast on ActivitiesFavoritesPage stays as a defensive
fallback for direct URL access.
2026-05-25 11:56:27 +02:00
f92d4090dd fix(activities): buy-tickets login toast gets a Log in action button
Matches the pattern already used by BookmarkButton, RSVPButton, and
ActivitiesFavoritesPage — the auth-prompt toast carries an inline
"Log in" action that pushes /login. The buy-tickets toast was the
odd one out, leaving the buyer to find the login route themselves.

Also lifts the message + button label into i18n
(activities.detail.loginToBuyTickets, .logIn) so es/fr aren't
English-on-top.
2026-05-25 11:56:11 +02:00
aa2e573f0e fix(activities): "Past events" chip narrows to past-only, not include-past
The chip should mirror the "My tickets" / "Hosting" mental model
(narrow to *only* X), not "additionally include X". Toggling ON with
"This Week" / "This Month" / "Tomorrow" was leaking upcoming events
through — confusing because the other role chips don't behave that
way.

Flip the past-events filter from a hide-when-off guard to a
side-of-now split: showPast=false → upcoming-only (default),
showPast=true → past-only. The DatePickerStrip override stays outside
this branch so date-pick still bypasses the split.
2026-05-25 11:46:19 +02:00
f6c15beb81 feat(activities): hide past events by default + "Past events" filter chip
Closes aiolabs/webapp#72.

useActivityFilters gains `showPast` (default false) and `togglePast`.
applyFilters drops activities whose end-or-start date is before now
unless the chip is toggled on. Sits next to the existing temporal
filter inside the no-selectedDate branch, so picking a specific past
date in the DatePickerStrip still surfaces that day's activities —
mirroring how date-pick already bypasses the temporal pills. Counts
as an active filter and resets cleanly.

ActivitiesPage adds the chip in the role-filter row, outside the
auth gate so logged-out users can still browse past events. Uses the
lucide `History` icon.

ActivityCard gains an `isPast` computed and renders a small Past
badge bottom-left when applicable, suppressed when a Pending /
Rejected status badge is already taking that slot (creator's own
past draft — vanishingly rare, status hint is more actionable).

ActivityDetailPage replaces the Buy ticket CTA with a muted "This
event has already happened" notice when the event is past, so the
buy flow stays unambiguous even when the user lands on a past
event by direct link. The owned-tickets pill above still renders so
past attendees can still see their tickets.

i18n: pastEvents (chip) + past (badge) + pastEvent (detail notice)
added to en/es/fr.
2026-05-25 11:37:46 +02:00
b4baad0d82 feat(activities): backend-truth counts + scanned list, tabs + popup result
The Scan Tickets page now sources its counts strip and scanned-ticket
roster from the new `events_list_event_tickets` RPC instead of a
per-device localStorage cache. The previous design diverged the
moment a second organizer scanned, or the operator switched from
mobile to laptop, or refreshed in incognito — backend truth keeps
all sessions consistent.

Webapp-side changes:

- useTicketScanner now exposes `eventStats` (sold / registered /
  remaining + per-ticket roster), `statsLoading`, `statsError`, and
  `refreshStats()`. Initial load on mount, refresh after every
  decode (success or failure) so the UI reflects state seconds
  after a scan lands.
- localStorage cache demoted to silent decode dedup only. The
  Clear-list button + its confirm dialog are gone — the cache
  isn't authoritative state to clear anymore.
- ScanTicketsPage gets two tabs: Scanner (camera + result) and
  Scanned ({count} from backend). Counts strip up top reads from
  `eventStats` (with the nostr-event `tickets_sold` tag as a
  fallback before the RPC roundtrip completes). A manual Refresh
  button in the top bar covers the rare case where a second device
  scans during your session.
- Result of each scan now lands as a full-viewport tap-to-dismiss
  overlay (success green / warning amber / destructive red) so
  the door operator can't skim past it on a busy entry.

Depends on aiolabs/events v1.6.1-aio.3 (already in the catalog).
2026-05-24 23:33:12 +02:00
815bc2d15f Merge pull request 'feat(activities): organizer ticket scanner over Nostr transport' (#73) from ticket-scanner-nostr-webapp into dev
Reviewed-on: #73
2026-05-24 16:51:12 +00:00
2498fbe518 fix(activities): pause scanner after each decode, require tap to scan next
Without a pause gate the qr-scanner's 5-fps decode loop instantly
fires another scan on whatever QR is still in frame — most
visibly, the ticket that just registered immediately re-fires as
"already scanned this session". The operator at the door never
gets a beat to confirm the result or act on it (let the attendee
in, deny entry, redirect to manual lookup, etc.).

useTicketScanner gains an `isPaused` ref that flips to true the
moment a decode resolves (success, error, or duplicate-session
de-dup) and gates further `onDecode` calls. The camera keeps
streaming so resumption is instant — only the decode handler is
muted.

The page replaces the small "Dismiss" button with a full-width
"Scan next" CTA below the result banner. Same place every time so
the operator's hand can stay in muscle memory; disabled while the
in-flight RPC is still sending. Result banner upgrades to a
slightly larger icon + label so the success/failure is readable
at arm's length over the venue.

`clearScanned` also resets `isPaused` so the operator can recover
from a stuck state via the "Clear list" affordance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 18:00:21 +02:00
5ebf0582e0 feat(activities): "Hosting" filter chip on the activities feed
Companion to the "My tickets" chip from #71. Where "My tickets"
narrows the feed to events you're attending, "Hosting" narrows it
to events you're organizing — reading `activity.isMine` which
useActivities.tagOwnership() already populates from organizer
pubkey match + own LNbits drafts.

Naming rationale: "My events" would have been ambiguous with
favorited / bookmarked. "Hosting" is short, role-oriented, and
pairs as the natural counterpart to "My tickets" (attending vs.
organizing). Spanish/French translations lean on the verb form
("Organizo" / "J'organise") since those languages don't have a
clean noun equivalent.

- useActivityFilters: onlyHosting flag, toggleHosting action,
  resetFilters clears it, hasActiveFilters lights up.
- applyFilters filters by `a.isMine === true` when the flag is
  on. Composes with category / temporal / "My tickets" via the
  same intersection chain.
- ActivitiesPage: chip rendered alongside "My tickets" with the
  Megaphone icon (lucide). Hidden when logged out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:58:12 +02:00
0f8f98d4c5 feat(activities): organizer ticket scanner over nostr-transport
Closes the activities loop: organizers scan attendees' QRs from the
standalone PWA at the door instead of dropping into the LNbits admin
register page. Every scan invokes the events_ticket_register RPC
(see aiolabs/events#19) over the nostr transport — the organizer's
signed kind-21000 event IS the authorization, no admin_key in the
browser.

- useTicketScanner: stateful driver. Parses `ticket://<id>` URIs,
  dedups in-session via localStorage (`activities_scanned_<id>`,
  mirroring the LNbits admin page's `events_scanned_<eventId>`
  pattern), surfaces lastScan with three states (ok / duplicate-
  session / error). Backend errors arrive as NostrRpcError messages
  ("Ticket not paid for", "Ticket already registered", etc.) and
  render directly.
- ScanTicketsPage: camera viewport + last-scan banner +
  scrollable session list with timestamps and (when available)
  ticket-holder names. Three banner variants (success/warning/
  destructive) so the organizer can read at a glance.
- Route /scan/:activityId, gated by requiresAuth. The "Scan" entry
  button on ActivityDetailPage's top bar is rendered only when
  `ownedLnbitsEvent !== null`, matching the existing "Edit" gating.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:46:48 +02:00
02c1be0ba7 feat(base): NostrTransportService — nip44 v2 kind-21000 RPC client for LNbits
Generic client for LNbits's nostr-transport (landed upstream Sun May
24, commit f235966c). Encrypts a request envelope to the server's
transport pubkey with NIP-44 v2, signs a kind-21000 event with the
current user's Nostr key, publishes via RelayHub, and listens for a
signed response addressed back to us. Shards (Lightning.Pub's
`{part, index, totalShards, shardsId}` wrapper) are reassembled
before parsing.

Activities ticket scanner is the first consumer; wallet ops + event
CRUD are obvious next adopters (file as follow-up). Server pubkey
discovery is currently env-var (VITE_LNBITS_NOSTR_TRANSPORT_PUBKEY)
— see also the follow-up to add a `.well-known` discovery endpoint
on LNbits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:46:31 +02:00