diff --git a/chatelet.html b/chatelet.html deleted file mode 100644 index b142652..0000000 --- a/chatelet.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - %VITE_APP_NAME% - - - - -
- - - diff --git a/package.json b/package.json index 8604d9e..d40f262 100644 --- a/package.json +++ b/package.json @@ -36,11 +36,8 @@ "dev:restaurant": "vite --host --config vite.restaurant.config.ts", "build:restaurant": "vue-tsc -b && vite build --config vite.restaurant.config.ts", "preview:restaurant": "vite preview --host --config vite.restaurant.config.ts", - "dev:chatelet": "vite --host --config vite.chatelet.config.ts", - "build:chatelet": "vue-tsc -b && vite build --config vite.chatelet.config.ts", - "preview:chatelet": "vite preview --host --config vite.chatelet.config.ts", - "dev:all": "concurrently -n hub,libra,events,wallet,chat,forum,market,tasks,restaurant,chatelet -c blue,magenta,cyan,yellow,green,blue,red,gray,green,cyan \"npm:dev\" \"npm:dev:libra\" \"npm:dev:events\" \"npm:dev:wallet\" \"npm:dev:chat\" \"npm:dev:forum\" \"npm:dev:market\" \"npm:dev:tasks\" \"npm:dev:restaurant\" \"npm:dev:chatelet\"", - "build:demo": "npm run build && VITE_BASE_PATH=/events/ npm run build:events && VITE_BASE_PATH=/libra/ npm run build:libra && VITE_BASE_PATH=/wallet/ npm run build:wallet && VITE_BASE_PATH=/chat/ npm run build:chat && VITE_BASE_PATH=/forum/ npm run build:forum && VITE_BASE_PATH=/market/ npm run build:market && VITE_BASE_PATH=/tasks/ npm run build:tasks && VITE_BASE_PATH=/restaurant/ npm run build:restaurant && VITE_BASE_PATH=/chatelet/ npm run build:chatelet", + "dev:all": "concurrently -n hub,libra,events,wallet,chat,forum,market,tasks,restaurant -c blue,magenta,cyan,yellow,green,blue,red,gray,green \"npm:dev\" \"npm:dev:libra\" \"npm:dev:events\" \"npm:dev:wallet\" \"npm:dev:chat\" \"npm:dev:forum\" \"npm:dev:market\" \"npm:dev:tasks\" \"npm:dev:restaurant\"", + "build:demo": "npm run build && VITE_BASE_PATH=/events/ npm run build:events && VITE_BASE_PATH=/libra/ npm run build:libra && VITE_BASE_PATH=/wallet/ npm run build:wallet && VITE_BASE_PATH=/chat/ npm run build:chat && VITE_BASE_PATH=/forum/ npm run build:forum && VITE_BASE_PATH=/market/ npm run build:market && VITE_BASE_PATH=/tasks/ npm run build:tasks && VITE_BASE_PATH=/restaurant/ npm run build:restaurant", "electron:dev": "concurrently \"vite --host\" \"electron-forge start\"", "electron:build": "vue-tsc -b && vite build && electron-builder", "electron:package": "electron-builder", diff --git a/src/chatelet-app/App.vue b/src/chatelet-app/App.vue deleted file mode 100644 index 0b1977e..0000000 --- a/src/chatelet-app/App.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/src/chatelet-app/app.config.ts b/src/chatelet-app/app.config.ts deleted file mode 100644 index da75c6f..0000000 --- a/src/chatelet-app/app.config.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { AppConfig } from '@/core/types' - -/** - * Standalone chatelet app configuration. - * Enables base + chatelet modules only. - */ -export const appConfig: AppConfig = { - modules: { - base: { - name: 'base', - enabled: true, - lazy: false, - config: { - nostr: { - relays: JSON.parse( - import.meta.env.VITE_NOSTR_RELAYS || - '["wss://relay.damus.io", "wss://nos.lol"]', - ), - }, - auth: { - sessionTimeout: 24 * 60 * 60 * 1000, - }, - pwa: { - autoPrompt: true, - }, - imageUpload: { - baseUrl: import.meta.env.VITE_PICTRS_BASE_URL || 'https://img.mydomain.com', - maxSizeMB: 10, - acceptedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/gif'], - }, - }, - }, - chatelet: { - name: 'chatelet', - enabled: true, - lazy: false, - config: { - apiConfig: { - baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || 'http://localhost:5000', - apiKey: import.meta.env.VITE_API_KEY || '', - }, - }, - }, - }, - - features: { - pwa: true, - pushNotifications: true, - electronApp: false, - developmentMode: import.meta.env.DEV, - }, -} - -export default appConfig diff --git a/src/chatelet-app/app.ts b/src/chatelet-app/app.ts deleted file mode 100644 index 13a76af..0000000 --- a/src/chatelet-app/app.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { createApp } from 'vue' -import { createRouter, createWebHistory } from 'vue-router' -import { createPinia } from 'pinia' -import { pluginManager } from '@/core/plugin-manager' -import { eventBus } from '@/core/event-bus' -import { container } from '@/core/di-container' - -import appConfig from './app.config' -import baseModule from '@/modules/base' -import chateletModule from '@/modules/chatelet' - -import App from './App.vue' - -import '@/assets/index.css' -import { i18n, changeLocale, type AvailableLocale } from '@/i18n' -import { installLenientAuthGuard, markAuthReady, catchAllRoute } from '@/lib/router-helpers' -import { acceptTokenFromUrl } from '@/lib/url-token' - -const APP_NAME = (import.meta.env.VITE_APP_NAME as string) || 'Chatelet' -const APP_LABEL = - APP_NAME.toLowerCase() === 'chatelet' ? 'Chatelet' : `Chatelet (${APP_NAME})` - -/** - * Initialize the standalone chatelet app (guest room booking). - */ -export async function createAppInstance() { - console.log(`🚀 Starting ${APP_LABEL}...`) - - // Accept token from URL before anything else (cross-subdomain auth relay) - acceptTokenFromUrl(APP_NAME) - - const app = createApp(App) - - const moduleRoutes = [ - ...(baseModule.routes || []), - ...(chateletModule.routes || []), - ].filter(Boolean) - - const router = createRouter({ - history: createWebHistory(import.meta.env.BASE_URL), - routes: [ - { path: '/', redirect: '/chatelet' }, - { - path: '/login', - name: 'login', - component: - import.meta.env.VITE_DEMO_MODE === 'true' - ? () => import('@/pages/LoginDemo.vue') - : () => import('@/pages/Login.vue'), - meta: { requiresAuth: false }, - }, - ...moduleRoutes, - { - path: '/settings', - name: 'settings', - component: () => import('./views/SettingsPage.vue'), - meta: { requiresAuth: false }, - }, - catchAllRoute, - ], - }) - - installLenientAuthGuard(router) - - const pinia = createPinia() - - app.use(router) - app.use(pinia) - app.use(i18n) - - const defaultLocale = import.meta.env.VITE_DEFAULT_LOCALE as AvailableLocale | undefined - if (defaultLocale && !localStorage.getItem('user-locale')) { - await changeLocale(defaultLocale) - } - - pluginManager.init(app, router) - - const moduleRegistrations = [] - if (appConfig.modules.base.enabled) { - moduleRegistrations.push( - pluginManager.register(baseModule, appConfig.modules.base), - ) - } - if (appConfig.modules.chatelet?.enabled) { - moduleRegistrations.push( - pluginManager.register(chateletModule, appConfig.modules.chatelet), - ) - } - - await Promise.all(moduleRegistrations) - await pluginManager.installAll() - - const { auth } = await import('@/composables/useAuthService') - await auth.initialize() - markAuthReady(auth) - - app.config.errorHandler = (err, _vm, info) => { - console.error('Global error:', err, info) - eventBus.emit('app:error', { error: err, info }, 'app') - } - - if (appConfig.features.developmentMode) { - ;(window as any).__pluginManager = pluginManager - ;(window as any).__eventBus = eventBus - ;(window as any).__container = container - } - - console.log(`✅ ${APP_LABEL} initialized`) - return { app, router } -} - -export async function startApp() { - try { - const { app } = await createAppInstance() - app.mount('#app') - console.log(`🎉 ${APP_LABEL} started!`) - eventBus.emit('app:started', {}, 'app') - } catch (error) { - console.error(`💥 Failed to start ${APP_LABEL}:`, error) - document.getElementById('app')!.innerHTML = ` -
-

Failed to Start

-

${error instanceof Error ? error.message : 'Unknown error'}

-

Please refresh the page.

-
- ` - } -} diff --git a/src/chatelet-app/main.ts b/src/chatelet-app/main.ts deleted file mode 100644 index 790c248..0000000 --- a/src/chatelet-app/main.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { startApp } from './app' -import { registerSW } from 'virtual:pwa-register' -import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup' -import 'vue-sonner/style.css' - -cleanupStaleDevServiceWorkers() - -// PWA service worker with periodic updates -const intervalMS = 60 * 60 * 1000 // 1 hour -registerSW({ - onRegistered(r) { - r && - setInterval(() => { - r.update() - }, intervalMS) - }, - onOfflineReady() { - console.log( - `${(import.meta.env.VITE_APP_NAME as string) || 'Chatelet'} ready to work offline`, - ) - }, -}) - -startApp() diff --git a/src/chatelet-app/views/SettingsPage.vue b/src/chatelet-app/views/SettingsPage.vue deleted file mode 100644 index 22fcde0..0000000 --- a/src/chatelet-app/views/SettingsPage.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - diff --git a/src/core/di-container.ts b/src/core/di-container.ts index a038a43..f406812 100644 --- a/src/core/di-container.ts +++ b/src/core/di-container.ts @@ -175,9 +175,6 @@ export const SERVICE_TOKENS = { // Restaurant services RESTAURANT_API: Symbol('restaurantAPI'), RESTAURANT_NOSTR_SYNC: Symbol('restaurantNostrSync'), - - // Chatelet (room rentals) services - CHATELET_API: Symbol('chateletAPI'), } as const // Type-safe injection helpers diff --git a/src/modules/chatelet/composables/useChatelet.ts b/src/modules/chatelet/composables/useChatelet.ts deleted file mode 100644 index c93fc6d..0000000 --- a/src/modules/chatelet/composables/useChatelet.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { storeToRefs } from 'pinia' -import { SERVICE_TOKENS, tryInjectService } from '@/core/di-container' -import type { ChateletApiService } from '../services/ChateletApiService' -import type { AvailabilityResult, Room } from '../types/room' -import { useChateletStore } from '../stores/chatelet' - -export function useChatelet() { - const store = useChateletStore() - const { rooms, isLoading } = storeToRefs(store) - // DI, never a direct import (workspace rule). - const api = tryInjectService(SERVICE_TOKENS.CHATELET_API) - - async function loadRooms(): Promise { - if (!api) return - isLoading.value = true - try { - store.rooms = await api.getPublicRooms() - } finally { - isLoading.value = false - } - } - - async function getRoom(id: string): Promise { - if (!api) return null - return api.getPublicRoom(id) - } - - async function checkAvailability( - roomId: string, - checkIn: string, - checkOut: string, - ): Promise { - if (!api) return null - return api.checkAvailability(roomId, checkIn, checkOut) - } - - return { rooms, isLoading, loadRooms, getRoom, checkAvailability } -} diff --git a/src/modules/chatelet/index.ts b/src/modules/chatelet/index.ts deleted file mode 100644 index 60653c9..0000000 --- a/src/modules/chatelet/index.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { createModulePlugin } from '@/core/base/BaseModulePlugin' -import { SERVICE_TOKENS } from '@/core/di-container' -import { ChateletApiService, type ChateletApiConfig } from './services/ChateletApiService' - -export interface ChateletModuleConfig { - apiConfig: ChateletApiConfig -} - -/** - * Chatelet module — guest booking UI for the LNbits `chatelet` room-rentals - * extension. Slice 1: read-only discovery + availability over HTTP. - */ -export const chateletModule = createModulePlugin({ - name: 'chatelet', - version: '1.0.0', - dependencies: ['base'], - - routes: [ - { - path: '/chatelet', - name: 'chatelet', - component: () => import('./views/ChateletPage.vue'), - meta: { - title: (import.meta.env.VITE_APP_NAME as string) || 'Chatelet', - requiresAuth: false, - }, - }, - { - path: '/chatelet/:id', - name: 'chatelet-detail', - component: () => import('./views/ChateletDetailPage.vue'), - meta: { title: 'Room', requiresAuth: false }, - }, - ], - - onInstall: async (_app, options) => { - const config = options?.config as ChateletModuleConfig | undefined - if (!config) { - throw new Error('Chatelet module requires configuration') - } - const { container } = await import('@/core/di-container') - const api = new ChateletApiService(config.apiConfig) - container.provide(SERVICE_TOKENS.CHATELET_API, api) - }, - - onUninstall: async () => { - const { container } = await import('@/core/di-container') - container.remove(SERVICE_TOKENS.CHATELET_API) - }, -}) - -export default chateletModule - -export type { Room, AvailabilityResult } from './types/room' diff --git a/src/modules/chatelet/services/ChateletApiService.ts b/src/modules/chatelet/services/ChateletApiService.ts deleted file mode 100644 index 015252a..0000000 --- a/src/modules/chatelet/services/ChateletApiService.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { AvailabilityResult, Room } from '../types/room' - -export interface ChateletApiConfig { - baseUrl: string - apiKey: string -} - -/** - * HTTP client for the LNbits `chatelet` extension's guest surface. - * - * Plain config-wrapper (same pattern as events' TicketApiService) — not a - * BaseService. Slice 1 is read-only guest browsing over public endpoints; - * booking (POST /bookings) + status polling arrive in slice 2. - * - * Transport note: this is HTTP-for-now. The extension also exposes a - * kind-21000 Nostr-RPC surface (chatelet_room_list / _availability / …), but - * the webapp's RPC client is disabled post-lnbits#9 (see aiolabs/lnbits#64). - * When that's revived, only this class swaps HTTP→RPC — no view changes. - */ -export class ChateletApiService { - constructor(private config: ChateletApiConfig) {} - - /** Active rooms for guest browsing (operator-private fields stripped server-side). */ - async getPublicRooms(): Promise { - return this.request('/chatelet/api/v1/public/rooms', { method: 'GET' }) - } - - async getPublicRoom(id: string): Promise { - return this.request(`/chatelet/api/v1/public/rooms/${id}`, { method: 'GET' }) - } - - async checkAvailability( - roomId: string, - checkIn: string, - checkOut: string, - ): Promise { - return this.request('/chatelet/api/v1/availability', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ room_id: roomId, check_in: checkIn, check_out: checkOut }), - }) - } - - private async request(path: string, init: RequestInit = {}): Promise { - const headers: Record = { - accept: 'application/json', - // Guest endpoints are unauthenticated; only send a key if one is set. - ...(this.config.apiKey ? { 'X-API-KEY': this.config.apiKey } : {}), - ...((init.headers as Record) ?? {}), - } - - const response = await fetch(`${this.config.baseUrl}${path}`, { ...init, headers }) - - if (!response.ok) { - const error = await response - .json() - .catch(() => ({ detail: `Request failed: ${path}` })) - const message = - typeof error.detail === 'string' - ? error.detail - : Array.isArray(error.detail) - ? (error.detail[0]?.msg ?? 'Request failed') - : 'Request failed' - throw new Error(message) - } - - return response.json() - } -} diff --git a/src/modules/chatelet/stores/chatelet.ts b/src/modules/chatelet/stores/chatelet.ts deleted file mode 100644 index 86a857f..0000000 --- a/src/modules/chatelet/stores/chatelet.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineStore } from 'pinia' -import { ref } from 'vue' -import type { Room } from '../types/room' - -export const useChateletStore = defineStore('chatelet', () => { - const rooms = ref([]) - const isLoading = ref(false) - return { rooms, isLoading } -}) diff --git a/src/modules/chatelet/types/room.ts b/src/modules/chatelet/types/room.ts deleted file mode 100644 index 6800d6a..0000000 --- a/src/modules/chatelet/types/room.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Public shapes returned by the chatelet extension's guest endpoints. -// Note: operator-private fields (wallet, checkin_instructions) are stripped -// server-side by public_room_dict and never reach the client. - -export interface Room { - id: string - title: string - description: string - price_amount: number - price_currency: string - price_frequency: string - max_guests: number - min_nights: number - amenities: string[] - location: string - geohash: string - images: string[] - status: string - listing_event_id: string | null - created_at: string - updated_at: string -} - -export interface AvailabilityResult { - room_id: string - check_in: string - check_out: string - available: boolean - nights: number - quote_sat: number | null - quote_fiat: number | null - currency: string | null -} diff --git a/src/modules/chatelet/views/ChateletDetailPage.vue b/src/modules/chatelet/views/ChateletDetailPage.vue deleted file mode 100644 index 8b8f7e7..0000000 --- a/src/modules/chatelet/views/ChateletDetailPage.vue +++ /dev/null @@ -1,123 +0,0 @@ - - - diff --git a/src/modules/chatelet/views/ChateletPage.vue b/src/modules/chatelet/views/ChateletPage.vue deleted file mode 100644 index 04418ff..0000000 --- a/src/modules/chatelet/views/ChateletPage.vue +++ /dev/null @@ -1,80 +0,0 @@ - - - diff --git a/vite.chatelet.config.ts b/vite.chatelet.config.ts deleted file mode 100644 index 0d69fb8..0000000 --- a/vite.chatelet.config.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { fileURLToPath, URL } from 'node:url' -import vue from '@vitejs/plugin-vue' -import tailwindcss from '@tailwindcss/vite' -import { defineConfig, type Plugin } from 'vite' -import { VitePWA } from 'vite-plugin-pwa' -import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' -import { visualizer } from 'rollup-plugin-visualizer' -import { - brand, - brandAlias, - brandAppBannerAliasEntry, - brandAppLogoAliasEntry, - brandAssetsPlugin, - brandHubLogoAliasEntry, - brandManifestName, - resolveAppBanner, -} from './vite-branding' - -/** - * SPA fallback: rewrite dev-server requests to chatelet.html. - */ -function chateletHtmlPlugin(): Plugin { - return { - name: 'chatelet-html-rewrite', - configureServer(server) { - server.middlewares.use((req, _res, next) => { - const path = req.url ? req.url.split('?')[0] : '' - if ( - req.url && - !req.url.startsWith('/@') && - !req.url.startsWith('/src/') && - !req.url.startsWith('/node_modules/') && - !path.includes('.') - ) { - req.url = '/chatelet.html' - } - next() - }) - }, - } -} - -/** - * Vite config for the standalone chatelet app (guest room booking). - * - * Set VITE_BASE_PATH to deploy under a path prefix: - * VITE_BASE_PATH=/chatelet/ → app.domain/chatelet/ (shared auth) - * (default: /) → standalone subdomain - */ -const APP_NAME = brandManifestName() -process.env.VITE_APP_NAME = APP_NAME -process.env.VITE_APP_BANNER = resolveAppBanner('chatelet') ? '1' : '' - -export default defineConfig(({ mode }) => ({ - base: process.env.VITE_BASE_PATH || '/', - cacheDir: 'node_modules/.vite-chatelet', - server: { - port: 5188, - strictPort: true, - }, - plugins: [ - brandAssetsPlugin(), - chateletHtmlPlugin(), - vue(), - tailwindcss(), - VitePWA({ - registerType: 'autoUpdate', - devOptions: { - enabled: false, - }, - workbox: { - globPatterns: ['**/*.{js,css,html,ico,png,svg}'], - navigateFallback: 'chatelet.html', - navigateFallbackAllowlist: [ - new RegExp(`^${(process.env.VITE_BASE_PATH || '/').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`), - ], - }, - includeAssets: [ - 'icons/favicon.ico', - 'icons/apple-touch-icon.png', - 'icons/icon-192.png', - 'icons/icon-512.png', - 'icons/icon-maskable-192.png', - 'icons/icon-maskable-512.png', - ], - manifest: { - name: APP_NAME, - short_name: brand.shortName ?? APP_NAME, - description: 'Book a room', - theme_color: brand.themeColor ?? '#1f2937', - background_color: brand.backgroundColor ?? '#ffffff', - display: 'standalone', - orientation: 'portrait-primary', - start_url: process.env.VITE_BASE_PATH || '/', - scope: process.env.VITE_BASE_PATH || '/', - id: 'aiolabs-chatelet', - categories: ['travel', 'lifestyle'], - icons: [ - { src: 'icons/icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' }, - { src: 'icons/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' }, - { src: 'icons/icon-maskable-192.png', sizes: '192x192', type: 'image/png', purpose: 'maskable' }, - { src: 'icons/icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, - ], - }, - }), - ViteImageOptimizer({ - jpg: { quality: 80 }, - png: { quality: 80 }, - webp: { lossless: true }, - }), - mode === 'analyze' && - visualizer({ - open: true, - filename: 'dist-chatelet/stats.html', - gzipSize: true, - brotliSize: true, - }), - ], - resolve: { - alias: [ - brandAppLogoAliasEntry('chatelet'), - brandAppBannerAliasEntry('chatelet'), - brandHubLogoAliasEntry(), - ...Object.entries(brandAlias).map(([find, replacement]) => ({ find, replacement })), - { find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) }, - ], - }, - build: { - outDir: 'dist-chatelet', - rollupOptions: { - input: 'chatelet.html', - output: { - manualChunks: { - 'vue-vendor': ['vue', 'vue-router', 'pinia'], - 'ui-vendor': ['radix-vue', '@vueuse/core'], - 'shadcn': ['class-variance-authority', 'clsx', 'tailwind-merge'], - }, - }, - }, - chunkSizeWarningLimit: 1000, - }, -}))