Tasks module's useful actions (create, claim, complete) all require
signing keys, so the tile should follow the wallet/chat/castle
ghosting pattern rather than the public-browsable forum/market/
activities pattern. Read-only browsing of tasks via the standalone
remains possible — only the hub's affordance changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 7 standalone vite configs had server.port + strictPort: true
since commit 9a1e5e3, but the hub config was left on the default
auto-incrementing 5173. When something briefly held 5173 (an
orphaned vite process from a crashed restart, an ENOENT during
concurrent dep optimization, etc.) the hub silently drifted to
5174. The browser's cached SW kept loading the page from
localhost:5173, all in-page asset fetches died with
ERR_CONNECTION_REFUSED, and chakra navigation appeared broken.
Pinning the hub the same way the standalones are pinned eliminates
that drift. If 5173 is genuinely held when dev:all starts, vite
will fail loud (and the user can free the port) instead of moving
the hub silently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related UX hardening tweaks for the unauthenticated case.
1. Module.authRequired flag
Tiles for modules with no public view (wallet, chat, castle) are
now ghosted out for unauthenticated visitors — same visual
treatment we already apply to "coming soon" tiles (opacity 60,
cursor not-allowed, non-anchored). This prevents an unauth user
from clicking through to a standalone that will instantly bounce
them to /login (per the strict guards in those apps).
Implementation: hubLink() returns null when authRequired &&
!isAuthenticated, which already triggers the existing non-link
render branch. No new visual treatment to design.
Public modules (forum, market, tasks, activities) and the
restaurant placeholder are unaffected.
2. Bottom-dock Profile↔Log-in swap
When logged in, the first dock slot opens the Profile sheet
(existing behaviour). When logged out it now renders a plain
Log-In button that pushes /login on the hub itself. Avoids
showing a "Profile" affordance to a user who has no profile yet.
Both changes localised to src/pages/Hub.vue. No other files
touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Centralizes guard installation in src/lib/router-helpers.ts and
applies five docs-recommended patterns uniformly across hub, wallet,
chat, market, tasks, forum, castle, and sortir.
1. Guard registration order
Vue Router docs: install guards before app.use(router). Was: each
app installed beforeEach() at the very end of createAppInstance(),
long after app.use(router) and after auth.initialize(). Worked
because mount happens last, but fragile.
Now: installLenientAuthGuard()/installStrictAuthGuard() runs
immediately after createRouter(), before app.use(router).
2. Return-based guard signatures
Vue Router 4 docs prefer returning a route location over the
next() callback (easier to misuse — forgot next() = hung
navigation, called twice = warning). Both helpers return paths
('/login', '/') or true to allow.
3. Removed misleading async on guards with no await
The old guards declared async (to, _from, next) => {...} but
never awaited anything. The new guards are genuinely async (they
await auth-readiness) so the async is justified.
4. Catch-all 404 route
Each router now ends with catchAllRoute = { path:
'/:pathMatch(.*)*', redirect: '/' }. Vue Router warns at runtime
if no catch-all is defined.
5. Auth-readiness deferred promise
Auth depends on services registered during
pluginManager.installAll() so it can't be imported at the top of
each app.ts. The helper exposes markAuthReady(auth) which
resolves a module-level promise; guards await this promise on
first invocation. Resolves the chicken-and-egg between
"guards-before-router" (Vue Router docs) and
"auth-after-services" (our DI lifecycle). Each app calls
markAuthReady() right after auth.initialize() succeeds.
Strict (wallet, chat, castle): every non-/login route requires auth.
Lenient (hub, forum, market, tasks, activities): only routes with
meta.requiresAuth === true are gated.
Behavior is unchanged from commit 4605703 — this is a refactor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related dev-quality fixes that compose to remove a footgun.
1. Stale service worker self-cleanup (src/lib/dev-sw-cleanup.ts)
Even with VitePWA's devOptions.enabled now false (commit 613a925),
service workers registered during earlier dev sessions linger in
the browser and intercept navigations, often serving cached bundles
from the broken-config period. Manifested as: castle/chat/wallet
not redirecting to /login despite the new auth guard, forum/market
showing "Failed to Start: Cannot read properties of undefined" for
modules that aren't even in their standalone config, hub redirecting
to /market on refresh.
The new helper runs at app boot in dev only:
- enumerates navigator.serviceWorker.getRegistrations()
- unregisters every one of them
- clears caches.keys()
- reloads once (gated by sessionStorage to avoid loops)
In production builds it's a no-op — the legitimate SW registered
by virtual:pwa-register survives.
Wired into all 8 main.ts entry points (hub + 7 standalones).
2. Apple-mobile-web-app-capable deprecation (.html)
Browsers now warn that <meta name="apple-mobile-web-app-capable">
should be paired with the standardized <meta name="mobile-web-app-capable">.
Adding the standardized tag alongside (kept the apple variant for
older iOS Safari) on all 8 HTML entry points.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Was: every standalone (and the hub) registered a service worker
during \`npm run dev\` via VitePWA's devOptions.enabled = true.
Problem: the dev SW caches index.html and the JS bundle on first
load and survives across vite restarts. Any code change that
required a server restart (e.g. fixing a vite.config.ts merge
conflict) resulted in browsers continuing to serve the cached
pre-restart bundle until the user manually unregistered the SW.
This caused the hub at localhost:5173 to redirect to /market on
refresh — the cached bundle was from the broken-config period
which still had the old monolithic main app's market route.
PWA features (offline, install prompts, manifest) are still tested
by running:
npm run preview # for the hub
npm run preview:<name> # for any standalone
against a real production build, which is the more accurate
environment for PWA verification anyway.
Recovery for anyone with a stale SW lingering in their browser
(needed once after pulling, then never again):
1. DevTools → Application → Service Workers → Unregister
2. DevTools → Application → Storage → Clear site data
3. Hard reload (Ctrl-Shift-R)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merge commit 13ad692 ("merge: forum standalone") committed
vite.config.ts with unresolved <<<<<<< / ||||||| / >>>>>>> markers
in the navigateFallbackDenylist regex array. Vite couldn't parse
the file, so the hub dev server failed to restart on config changes
and kept serving stale code from before the merge — including the
old monolithic main app's /market route, which manifested as a
mysterious redirect from / → /market for users testing the hub.
Resolution: keep the union of all three sides
(sortir, castle, wallet, chat, market, cart, checkout, tasks,
forum, submit, submission).
Recovery for anyone seeing the stale /market redirect after pulling:
- hard-reload the browser (Cmd/Ctrl-Shift-R)
- DevTools → Application → Service Workers → Unregister
- Re-run npm run dev (or dev:all) — the hub now restarts cleanly
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
These three standalone apps have no meaningful public view — wallet
needs the LNbits token to do anything, chat needs Nostr keys to
decrypt DMs, castle's accounting only makes sense for an account
holder. Their previous router guards only redirected when a route
explicitly opted in via meta.requiresAuth: an unauth user could land
on the home page and see broken / empty content with no signal.
Replaces each app's per-route guard with a strict policy: any
navigation to a path other than /login requires auth, otherwise
bounce to /login. /login itself bounces an authenticated user back
to /.
Affected guards:
- src/wallet-app/app.ts
- src/chat-app/app.ts
- src/accounting-app/app.ts
Forum / market / tasks / activities keep the existing per-route
guard so they remain browseable without an account by default.
That browsing-vs-auth choice will become operator-configurable per
deployment (tracked separately).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reserves a top-right corner badge on each chakra tile, hidden when
the module has no unread items. The Module interface gains an
optional \`unread?: number\`; tiles render a 18×18 red pill with the
count (capped at "99+") in the top-right when unread > 0.
No data source yet — this is a placeholder slot. Wires to the
per-standalone notification feeds defined in #32: each standalone
will publish its unread count, hub aggregates and projects into the
modules array. Until then every tile renders without a badge.
Picked a red pill over the theme's primary because red is the
universal "unread" signal across iOS / Slack / Discord / Gmail.
Ring-1 ring-background gives a subtle halo against any tile shade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixed-port assignments for each standalone vite dev server, with
strictPort to fail loud if a port is taken (no silent +1 increment
that would break the hub's hardcoded VITE_HUB_<NAME>_URL targets):
hub 5173 (npm run dev)
castle 5180
sortir 5181 (activities)
wallet 5182
chat 5183
forum 5184
market 5185
tasks 5186
`npm run dev:all` boots the hub and all 7 standalones concurrently
via the existing concurrently devDep. The hub's chakra tiles point
at these ports via VITE_HUB_<NAME>_URL in .env.local for end-to-end
local testing of the cross-subdomain auth relay.
Pure dev infrastructure — no production behaviour change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AVAILABLE_LOCALES advertised 'de' and 'zh' but src/i18n/locales/
only ships en.ts, es.ts, fr.ts. Selecting de or zh from the new
hub language picker would 404 the dynamic import.
- src/i18n/index.ts: AVAILABLE_LOCALES = ['en', 'es', 'fr']
- src/composables/useLocale.ts: trim flag map to match
VITE_DEFAULT_LOCALE still drives first-run default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The all-in-one app at app.${domain} is a minimal hub: only the base
module (auth, profile, relays, PWA, image upload) plus a chakra-
themed entry point linking out to the seven standalone module PWAs
(market, sortir, wallet, chat, forum, tasks, castle), with an
eighth tile reserved for a forthcoming restaurant module.
UI:
- 2-column grid of 8 module tiles with Lucide icons, occupying the
full viewport between the title and the bottom dock. Status hints
(alpha/beta/coming soon) shown beneath each label.
- Faint chakra-mandala column rendered behind the tiles (peeks
through their translucent backgrounds), plus a subtle vertical
hue gradient (red at the bottom → violet at the top) — the chakras
inform the visual frame without forcing a 1:1 module mapping.
- Bottom dock with system-level controls: Profile (Sheet hosting
the existing ProfileSettings.vue), Theme (light/dark/system),
Language (uses available locales), and a Currency placeholder.
- Each tile is a link to VITE_HUB_<NAME>_URL with the user's
lnbits_access_token appended as ?token= so the destination logs
in via the existing acceptTokenFromUrl() relay.
Wiring:
- src/App.vue: stripped to the same minimal shell as the standalone
apps (no AppLayout/AppSidebar — the hub is the navigation).
- src/app.ts: only base module is registered. Hub is the / route.
- src/app.config.ts: only base module config remains.
- public/chakras/*.svg: 7 chakra mandala SVGs copied from the legacy
frontend (Atitlan.io).
- nginx.conf.example: rewritten with one server block per subdomain
pointing at its own dist-<name>/ output.
Closes#26.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tabs at the bottom of the forum standalone:
Posts (→ /forum), Spaces, Submit (→ /submit), Search, Alerts
Spaces, Search, and Alerts are dimmed and emit a "coming soon" toast
on tap pointing at the tracking issue:
- Spaces → #31 (NIP-72 communities)
- Search → #15 (link aggregator search)
- Alerts → #32 (per-standalone notifications, hub aggregation)
Mirrors the activities-app bottom-bar pattern (icon + 10px label,
fixed bottom, safe-area-aware) and replaces the previous bare
forum-app shell which had no way to compose a new submission.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a standalone forum PWA at forum.${domain}, built from the
existing src/modules/forum plugin (NIP-72 communities + kind 1111
posts + voting). Same standalone pattern as the other modules:
- forum.html entry, vite.forum.config.ts (outDir: dist-forum,
manifest id: forum-app, theme: blue #2563eb — Vishuddha chakra)
- src/forum-app/{main.ts, app.ts, app.config.ts, App.vue} bootstraps
base + forum only, with acceptTokenFromUrl for shared auth from hub
- new ForumListPage view + /forum route added to the forum module
(previously the list was only embedded in Home.vue)
- npm run dev:forum / build:forum / preview:forum
- main app SW denylist extended with /forum/, /submit/, /submission/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a standalone tasks PWA at tasks.${domain}, built from the
existing src/modules/tasks plugin (Nostr calendar events, kind
31922/31925). Same standalone pattern as the other modules:
- tasks.html entry, vite.tasks.config.ts (outDir: dist-tasks,
manifest id: tasks-app, theme: indigo #4338ca — Ajna chakra)
- src/tasks-app/{main.ts, app.ts, app.config.ts, App.vue} bootstraps
base + tasks only, with acceptTokenFromUrl for shared auth from hub
- npm run dev:tasks / build:tasks / preview:tasks
- main app SW denylist extended with /tasks/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a standalone Nostr marketplace PWA at market.${domain}, built
from the existing src/modules/market plugin. Same standalone pattern
as wallet/chat/castle/activities:
- market.html entry, vite.market.config.ts (outDir: dist-market,
manifest id: market-app, theme: red #dc2626 — Muladhara chakra)
- src/market-app/{main.ts, app.ts, app.config.ts, App.vue} bootstraps
base + market only, with acceptTokenFromUrl for shared auth from hub
- npm run dev:market / build:market / preview:market
- main app SW denylist extended with /market/, /cart/, /checkout/
Closes#18.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a standalone encrypted chat PWA at chat.${domain}, built from
the existing src/modules/chat plugin. Same standalone pattern as
wallet/castle/activities:
- chat.html entry, vite.chat.config.ts (outDir: dist-chat,
manifest id: chat-app, theme: green #16a34a — Anahata chakra)
- src/chat-app/{main.ts, app.ts, app.config.ts, App.vue} bootstraps
base + chat only, with acceptTokenFromUrl for shared auth from hub
- npm run dev:chat / build:chat / preview:chat
- main app SW denylist extended with /chat/
Closes#20.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a standalone Lightning wallet PWA at wallet.${domain}, built
from the existing src/modules/wallet plugin. Mirrors the Castle and
Activities standalone patterns:
- wallet.html entry, vite.wallet.config.ts (outDir: dist-wallet,
manifest id: wallet-app, theme: yellow #eab308 — Manipura chakra)
- src/wallet-app/{main.ts, app.ts, app.config.ts, App.vue} bootstraps
base + wallet only, with acceptTokenFromUrl for shared auth from hub
- npm run dev:wallet / build:wallet / preview:wallet
- main app SW denylist extended with /wallet/
Closes#19.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The links module already implements a reddit-style link aggregator
(NIP-72 communities + kind 1111 + voting). Renaming to forum aligns
the module name with its purpose ahead of extracting it as a
standalone PWA at forum.${domain}.
This is a pure rename — no behavior changes. Affects:
- src/modules/links → src/modules/forum (git mv)
- linksModule export → forumModule
- appConfig.modules.links → appConfig.modules.forum
- log/event source strings: 'links' → 'forum'
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When multiple SPAs share the same origin (e.g., /sortir/ and /castle/
on demo.aiolabs.dev), their service workers conflict. Each app's
workbox now scopes its navigateFallback with navigateFallbackAllowlist,
and the main app excludes standalone paths via navigateFallbackDenylist.
- Main app: denylist /sortir/ and /castle/ from its service worker
- Sortir: allowlist only /sortir/ paths, fallback to activities.html
- Castle: allowlist only /castle/ paths, fallback to castle.html
- Icon paths use relative URLs (work with any base path)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shows a LogIn icon next to the refresh button on the activities
page when the user isn't logged in.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
event_end_date is now optional (null when not provided). Update
formatDate to accept null, and pastEvents filter to fall back
to event_start_date.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove proposeEvent(), consolidate to createEvent() with invoice key.
Backend determines approval status based on user role and settings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace CreateActivityDialog (direct Nostr publish) with
CreateEventDialog (LNbits propose flow) on ActivitiesPage.
Users submit events via POST /events/propose with invoice key.
Admin reviews and approves before events go live.
Add proposeEvent() to TicketApiService for invoice-key auth.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Only title and start date are required
- Description, location, categories, image, tickets visible by default
- End date and promo codes in collapsible "More options" section
- Categories use badge toggles matching the activities module
- Use ScrollArea for proper shadcn scrolling
- Update CreateEventRequest and TicketedEvent types for new fields
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Standalone apps now conditionally load LoginDemo.vue when
VITE_DEMO_MODE=true, matching the main app's behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add VITE_BASE_PATH support to standalone app configs so they can be
served under a path prefix (e.g., app.domain.com/sortir/) instead of
a separate subdomain. This enables shared auth via same-origin
localStorage.
- Vite configs: configurable base path via VITE_BASE_PATH env var
- Routers: use import.meta.env.BASE_URL for history base
- PWA manifests: use base path for start_url and scope
- Default: / (backward compatible with subdomain mode)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Filter by both event-type:task tag and presence of status tag
(NIP-52 calendar events don't have status on kind 31922, only
on RSVP kind 31925). This catches manually-created task events
that may not have the event-type tag.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tasks reuse NIP-52 kind 31922 but tag events with event-type:task.
Filter these out in parseNostrEventToActivity so household chores
don't show up as community activities.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The GET /tickets/{event_id}/user/{user_id} endpoint was a custom
addition not present in the upstream LNbits events extension. Use
the canonical POST /tickets/{event_id} with user_id in the body.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The activities module was only registered in the standalone Sortir app,
not the main app. When events module was removed, its registration was
deleted but activities was never added in its place.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Absorb the disabled events module into the active activities module,
eliminating duplicated LNbits events extension API surface and legacy
imports. Activities now owns all ticketing UI (EventsPage, MyTicketsPage
with QR codes, PurchaseTicketDialog, CreateEventDialog) alongside its
existing Nostr NIP-52 calendar event discovery.
- Internalize payInvoiceWithWallet in PaymentService (core LNbits endpoint)
- Enhance TicketApiService with createEvent and getCurrencies methods
- Add TICKET_API DI token for canonical ticket service access
- Port composables (useTicketPurchase, useUserTickets, useEvents) with DI
- Port components (PurchaseTicketDialog, CreateEventDialog)
- Replace MyTicketsPage placeholder with full QR-code ticket display
- Add /events route to activities module
- Delete src/modules/events/, src/lib/api/events.ts, src/lib/types/event.ts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Castle API returns positive = user owes castle, negative = castle owes
user. The display was inverted, showing "you owe" after submitting an
expense when it should show "owed to you".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restores LoginDemo.vue from the demo-login branch. When
VITE_DEMO_MODE=true, the main app uses LoginDemo.vue which allows
instant demo account creation with auto-generated credentials.
Production deployments are unaffected (flag defaults to false).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New standalone app at castle.html with its own Vite config, app shell,
and bottom nav (Record, Transactions, Balance, Wallet, Settings). Reuses
existing expenses and wallet modules with base module for a focused
accounting experience.
Features:
- Expense submission via existing AddExpense dialog
- Income submission placeholder (feature-flagged, pending backend)
- Balance page with pending expense tracking
- Expense drafts with receipt photo upload and BTC price snapshots
- Cross-subdomain auth token relay via ?token= URL parameter
- i18n support (en/fr/es)
- PWA with offline support
Also adds token relay to activities-app for consistent cross-app auth.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All hardcoded English strings now use t() with i18n keys: bottom nav
tabs, search placeholder, empty states, favorites page, settings page.
Added useDateLocale composable that maps the current i18n language to
the corresponding date-fns locale (fr/es/enUS). All date formatting
across ActivityCard, ActivityDetailPage, DatePickerStrip, calendar
view, and search overlay now passes the locale to date-fns format().
Month names, day names, and date labels change with language switch.
Added nav, search, favorites, and settings i18n sections to all three
locales (EN/FR/ES).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New ActivitySearchOverlay component: compact dropdown anchored to the
search input showing thumbnail, title, date, and location for each
result. Limited to 8 results, scrollable. Stays visible above the
keyboard on mobile. Tap a result to navigate, tap outside or clear
to dismiss. Uses the same Fuse.js fuzzy search under the hood.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>