## 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
3.9 KiB
Publishing & confirmation
Treat result.success === 0 as failure, not success
Canonical: src/modules/events/composables/useRSVP.ts —
if (!result || result.success <= 0) return null.
const result = await relayHub.publishEvent(signedEvent)
if (!result || result.success <= 0) return null // no relay accepted
// only now mutate local cache
Why: RelayHub.publishEvent returns { success: N, total: M }.
success > 0 means at least one relay sent OK. success === 0 means every
connected relay rejected (rate limit, signature failure, or the relay was
just down). Updating local state on a 0-success publish leaves the UI ahead
of every relay — on next refresh the user sees their action has vanished.
Some implementations (relay-hub.ts ~530–551) throw on success === 0
instead. Either contract works; pick one and be consistent within a
composable. Don't write code that silently treats both as success.
Optimistic-on-success, not optimistic-on-click
Canonical: src/modules/events/composables/useRSVP.ts — local
cache update after the await resolves with success > 0, before the
relay echoes the event back through the subscription.
The window we're closing is between "publish succeeded" and "subscription delivers the event back to us". That window can be 50ms or 5s depending on relays. Updating the cache there gives instant UI feedback without lying: if the publish actually fails, the cache stays untouched and the UI never showed the false state.
Avoid pre-publish optimistic updates ("flip the button on click, roll back on failure"). Rollback is jarring, and on slow connections users see the button flip twice.
Pending-coord debounce: disable the button during in-flight publish
Canonical: src/modules/events/composables/useRSVP.ts —
pendingCoords: ref<Set<string>> + isPending(...) predicate +
try { … } finally { pendingCoords.value.delete(coord) }.
if (pendingCoords.value.has(coord)) return null // throttle
pendingCoords.value.add(coord)
try {
await publish…
} finally {
pendingCoords.value.delete(coord)
}
Bind :disabled="pending" on the button. This pairs with the
monotonic created_at — together they make a series of rapid clicks well-defined: one in flight at a
time, each strictly newer than the last.
Why finally: if the publish throws, you must still release the lock or the button is stuck disabled forever.
Why per-coord, not global: the user might click RSVP on activity A
while a previous publish on activity B is still flying. A global
isPending would block that.
Sign with nostr-tools.finalizeEvent, take privkey as bytes
Canonical: src/modules/events/composables/useRSVP.ts —
hexToUint8Array helper + finalizeEvent(template, signingKey).
finalizeEvent expects a Uint8Array, not a hex string. Several composables
duplicate the hex→bytes helper inline; centralize when you have a third
caller (the duplication is fine for two).
Validate before signing: check currentUser.value?.prvkey is set, and
guard against logging the privkey or its derived bytes anywhere
(intentionally or via console.log(JSON.stringify(...))). Keys never leave
the auth service except into finalizeEvent arguments.
If the project ever gains NIP-07/NIP-46 support, the signing call site is where you'd branch on auth method. Until then, prvkey-in-memory is the only path.
Publish-only-once vs publish-to-many-relays
RelayHub.publishEvent fans out to all connected healthy relays. Don't
loop in user code; the hub handles the fan-out, dedup, and timeout. The
return aggregates outcomes ({ success, total }).
If you need to target a specific relay (rare — usually for migration or a custom personal relay), use the relay's lower-level API directly and skip the hub. Document why in a comment, because it breaks the visibility/ restoration story above.