From 9a3e3ae0ed4220aa98716914208fa2cd8f75b24a Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 2 May 2026 09:04:14 +0200 Subject: [PATCH] feat: minimal AIO hub with chakra grid + bottom dock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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__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-/ output. Closes #26. Co-Authored-By: Claude Opus 4.7 (1M context) --- nginx.conf.example | 122 ++++++++++++++++---- public/chakras/ajna.svg | 64 +++++++++++ public/chakras/anahata.svg | 108 ++++++++++++++++++ public/chakras/manipura.svg | 92 +++++++++++++++ public/chakras/muladhara.svg | 76 +++++++++++++ public/chakras/sahasrara.svg | 132 ++++++++++++++++++++++ public/chakras/swadhisthana.svg | 84 ++++++++++++++ public/chakras/vishuddha.svg | 124 ++++++++++++++++++++ src/App.vue | 80 ++++--------- src/app.config.ts | 125 ++------------------ src/app.ts | 157 +++----------------------- src/pages/Hub.vue | 194 ++++++++++++++++++++++++++++++++ 12 files changed, 1025 insertions(+), 333 deletions(-) create mode 100644 public/chakras/ajna.svg create mode 100644 public/chakras/anahata.svg create mode 100644 public/chakras/manipura.svg create mode 100644 public/chakras/muladhara.svg create mode 100644 public/chakras/sahasrara.svg create mode 100644 public/chakras/swadhisthana.svg create mode 100644 public/chakras/vishuddha.svg create mode 100644 src/pages/Hub.vue diff --git a/nginx.conf.example b/nginx.conf.example index e22b16e..05cf1f6 100644 --- a/nginx.conf.example +++ b/nginx.conf.example @@ -1,45 +1,125 @@ # Main context -worker_processes auto; # Automatically determine worker processes based on CPU cores +worker_processes auto; events { - worker_connections 1024; # Maximum connections per worker + worker_connections 1024; } http { default_type application/octet-stream; - # Trust the custom Docker network subnet set_real_ip_from 0.0.0.0; real_ip_header X-Forwarded-For; real_ip_recursive on; + # Reusable location blocks + # JS / CSS / image MIME and caching + map $sent_http_content_type $cache_static { + default "off"; + ~image/ "6M"; + } + + # ─────────────────────────────────────────────────────────────── + # AIO hub — minimal app at app. + # Serves only the chakra icon hub + base infra (profile, relays). + # ─────────────────────────────────────────────────────────────── server { listen 8080; - server_name .; + server_name app..; - root /app; + root /var/www/aio/dist; index index.html; - location / { - try_files $uri $uri/ /index.html; - } - - location ~* \.js$ { - types { application/javascript js; } - default_type application/javascript; + location / { try_files $uri $uri/ /index.html; } + location ~* \.js$ { types { application/javascript js; } default_type application/javascript; } + location ~* \.css$ { types { text/css css; } default_type text/css; } + location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; } } - # Serve CSS files with the correct MIME type - location ~* \.css$ { - types { text/css css; } - default_type text/css; + # ─────────────────────────────────────────────────────────────── + # Standalone module PWAs — one server block per subdomain + # ─────────────────────────────────────────────────────────────── + + # Marketplace — Muladhara + server { + listen 8080; + server_name market..; + root /var/www/aio/dist-market; + index market.html; + location / { try_files $uri $uri/ /market.html; } + location ~* \.js$ { types { application/javascript js; } default_type application/javascript; } + location ~* \.css$ { types { text/css css; } default_type text/css; } + location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; } } - # Serve image files - location ~* \.(png|jpe?g|webp|ico)$ { - expires 6M; # Optional: Cache static assets for 6 months - access_log off; + # Activities — Swadhisthana + server { + listen 8080; + server_name sortir..; + root /var/www/aio/dist-activities; + index activities.html; + location / { try_files $uri $uri/ /activities.html; } + location ~* \.js$ { types { application/javascript js; } default_type application/javascript; } + location ~* \.css$ { types { text/css css; } default_type text/css; } + location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; } } + # Wallet — Manipura + server { + listen 8080; + server_name wallet..; + root /var/www/aio/dist-wallet; + index wallet.html; + location / { try_files $uri $uri/ /wallet.html; } + location ~* \.js$ { types { application/javascript js; } default_type application/javascript; } + location ~* \.css$ { types { text/css css; } default_type text/css; } + location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; } + } + + # Chat — Anahata + server { + listen 8080; + server_name chat..; + root /var/www/aio/dist-chat; + index chat.html; + location / { try_files $uri $uri/ /chat.html; } + location ~* \.js$ { types { application/javascript js; } default_type application/javascript; } + location ~* \.css$ { types { text/css css; } default_type text/css; } + location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; } + } + + # Forum — Vishuddha + server { + listen 8080; + server_name forum..; + root /var/www/aio/dist-forum; + index forum.html; + location / { try_files $uri $uri/ /forum.html; } + location ~* \.js$ { types { application/javascript js; } default_type application/javascript; } + location ~* \.css$ { types { text/css css; } default_type text/css; } + location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; } + } + + # Tasks — Ajna + server { + listen 8080; + server_name tasks..; + root /var/www/aio/dist-tasks; + index tasks.html; + location / { try_files $uri $uri/ /tasks.html; } + location ~* \.js$ { types { application/javascript js; } default_type application/javascript; } + location ~* \.css$ { types { text/css css; } default_type text/css; } + location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; } + } + + # Castle — Sahasrara (accounting) + server { + listen 8080; + server_name castle..; + root /var/www/aio/dist-castle; + index castle.html; + location / { try_files $uri $uri/ /castle.html; } + location ~* \.js$ { types { application/javascript js; } default_type application/javascript; } + location ~* \.css$ { types { text/css css; } default_type text/css; } + location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; } } } - diff --git a/public/chakras/ajna.svg b/public/chakras/ajna.svg new file mode 100644 index 0000000..212d4e4 --- /dev/null +++ b/public/chakras/ajna.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + diff --git a/public/chakras/anahata.svg b/public/chakras/anahata.svg new file mode 100644 index 0000000..a2cc52b --- /dev/null +++ b/public/chakras/anahata.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/chakras/manipura.svg b/public/chakras/manipura.svg new file mode 100644 index 0000000..0c87aa8 --- /dev/null +++ b/public/chakras/manipura.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/chakras/muladhara.svg b/public/chakras/muladhara.svg new file mode 100644 index 0000000..cd21ee8 --- /dev/null +++ b/public/chakras/muladhara.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + diff --git a/public/chakras/sahasrara.svg b/public/chakras/sahasrara.svg new file mode 100644 index 0000000..f8f6323 --- /dev/null +++ b/public/chakras/sahasrara.svg @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/chakras/swadhisthana.svg b/public/chakras/swadhisthana.svg new file mode 100644 index 0000000..9123a7d --- /dev/null +++ b/public/chakras/swadhisthana.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/chakras/vishuddha.svg b/public/chakras/vishuddha.svg new file mode 100644 index 0000000..7b71e72 --- /dev/null +++ b/public/chakras/vishuddha.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.vue b/src/App.vue index 559d0bf..8c7f8ba 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,77 +1,47 @@ diff --git a/src/app.config.ts b/src/app.config.ts index 9c50444..dce6464 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -1,12 +1,11 @@ import type { AppConfig } from './core/types' -function parseMapCenter(envValue: string | undefined, fallback: { lat: number; lng: number }) { - if (!envValue) return fallback - const [lat, lng] = envValue.split(',').map(Number) - if (isNaN(lat) || isNaN(lng)) return fallback - return { lat, lng } -} - +/** + * Minimal AIO hub configuration. + * The all-in-one app at app.${domain} ships only the base module — + * each feature module (wallet, chat, market, tasks, forum, activities, + * castle) is now its own standalone PWA at its own subdomain. + */ export const appConfig: AppConfig = { modules: { base: { @@ -18,7 +17,7 @@ export const appConfig: AppConfig = { relays: JSON.parse(import.meta.env.VITE_NOSTR_RELAYS || '["wss://relay.damus.io", "wss://nos.lol"]') }, auth: { - sessionTimeout: 24 * 60 * 60 * 1000, // 24 hours + sessionTimeout: 24 * 60 * 60 * 1000, }, pwa: { autoPrompt: true @@ -29,115 +28,9 @@ export const appConfig: AppConfig = { acceptedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/gif'] } } - }, - 'nostr-feed': { - name: 'nostr-feed', - enabled: false, // Disabled - replaced by forum module - lazy: false, - config: { - refreshInterval: 30000, - maxPosts: 100, - adminPubkeys: JSON.parse(import.meta.env.VITE_ADMIN_PUBKEYS || '[]'), - feedTypes: ['announcements', 'general'] - } - }, - forum: { - name: 'forum', - enabled: true, - lazy: false, - config: { - maxSubmissions: 50, - corsProxyUrl: import.meta.env.VITE_CORS_PROXY_URL || '', - adminPubkeys: JSON.parse(import.meta.env.VITE_ADMIN_PUBKEYS || '[]') - } - }, - tasks: { - name: 'tasks', - enabled: true, - lazy: false, - config: { - maxTasks: 200, - adminPubkeys: JSON.parse(import.meta.env.VITE_ADMIN_PUBKEYS || '[]') - } - }, - market: { - name: 'market', - enabled: true, - lazy: false, - config: { - defaultCurrency: 'sats', - paymentTimeout: 300000, // 5 minutes - maxOrderHistory: 50, - apiConfig: { - baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || 'http://localhost:5000' - } - } - }, - chat: { - name: 'chat', - enabled: true, - lazy: false, // Load on startup to register routes - config: { - maxMessages: 500, - autoScroll: true, - showTimestamps: true, - notifications: { - enabled: true, - soundEnabled: false, - wildcardSupport: true - } - } - }, - activities: { - name: 'activities', - enabled: true, - lazy: false, - config: { - apiConfig: { - baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || 'http://localhost:5000', - apiKey: import.meta.env.VITE_API_KEY || '' - }, - defaultMapCenter: parseMapCenter(import.meta.env.VITE_DEFAULT_MAP_CENTER, { lat: 46.6034, lng: 1.8883 }), - maxTicketsPerUser: 10, - enableMap: true, - enablePrivateEvents: false - } - }, - wallet: { - name: 'wallet', - enabled: true, - lazy: false, - config: { - defaultReceiveAmount: 1000, // 1000 sats - maxReceiveAmount: 1000000, // 1M sats - apiConfig: { - baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || 'http://localhost:5000' - }, - websocket: { - enabled: import.meta.env.VITE_WEBSOCKET_ENABLED !== 'false', // Can be disabled via env var - reconnectDelay: 2000, // 2 seconds (increased from 1s to reduce server load) - maxReconnectAttempts: 3, // Reduced from 5 to avoid overwhelming server - fallbackToPolling: true, // Enable polling fallback when WebSocket fails - pollingInterval: 10000 // 10 seconds for polling updates - } - } - }, - expenses: { - name: 'expenses', - enabled: true, - lazy: false, - config: { - apiConfig: { - baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || 'http://localhost:5000', - timeout: 30000 // 30 seconds for API requests - }, - defaultCurrency: 'sats', - maxExpenseAmount: 1000000, // 1M sats - requireDescription: true - } } }, - + features: { pwa: true, pushNotifications: true, @@ -146,4 +39,4 @@ export const appConfig: AppConfig = { } } -export default appConfig \ No newline at end of file +export default appConfig diff --git a/src/app.ts b/src/app.ts index bfc93fa..ebccb5c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,67 +1,43 @@ import { createApp } from 'vue' import { createRouter, createWebHistory } from 'vue-router' import { createPinia } from 'pinia' -// Core plugin system import { pluginManager } from './core/plugin-manager' import { eventBus } from './core/event-bus' import { container } from './core/di-container' -// App configuration import appConfig from './app.config' -// Base modules import baseModule from './modules/base' -import nostrFeedModule from './modules/nostr-feed' -import chatModule from './modules/chat' -import activitiesModule from './modules/activities' -import marketModule from './modules/market' -import walletModule from './modules/wallet' -import expensesModule from './modules/expenses' -import forumModule from './modules/forum' -import tasksModule from './modules/tasks' -// Root component import App from './App.vue' -// Styles import './assets/index.css' - -// Use existing i18n setup import { i18n } from './i18n' /** - * Initialize and start the modular application + * Initialize and start the minimal AIO hub. + * + * The all-in-one app at app.${domain} now ships only the base module + * plus a chakra icon hub linking out to the standalone module apps + * (wallet, chat, market, tasks, forum, activities, castle). */ export async function createAppInstance() { - console.log('🚀 Starting modular application...') + console.log('🚀 Starting AIO hub...') - // Create Vue app const app = createApp(App) - // Collect all module routes automatically to avoid duplication const moduleRoutes = [ - // Extract routes from modules directly ...baseModule.routes || [], - ...nostrFeedModule.routes || [], - ...chatModule.routes || [], - ...activitiesModule.routes || [], - ...marketModule.routes || [], - ...walletModule.routes || [], - ...expensesModule.routes || [], - ...forumModule.routes || [], - ...tasksModule.routes || [] ].filter(Boolean) - // Create router with all routes available immediately const router = createRouter({ history: createWebHistory(), routes: [ - // Default routes { path: '/', - name: 'home', - component: () => import('./pages/Home.vue'), - meta: { requiresAuth: true } + name: 'hub', + component: () => import('./pages/Hub.vue'), + meta: { requiresAuth: false } }, { path: '/login', @@ -71,175 +47,74 @@ export async function createAppInstance() { : () => import('./pages/Login.vue'), meta: { requiresAuth: false } }, - // Pre-register module routes ...moduleRoutes ] }) - // Use existing i18n setup - - // Create Pinia store const pinia = createPinia() - // Install core plugins app.use(router) app.use(pinia) app.use(i18n) - // Initialize plugin manager pluginManager.init(app, router) - // Register modules based on configuration const moduleRegistrations = [] - // Register base module first (required) if (appConfig.modules.base.enabled) { moduleRegistrations.push( pluginManager.register(baseModule, appConfig.modules.base) ) } - // Register nostr-feed module - if (appConfig.modules['nostr-feed'].enabled) { - moduleRegistrations.push( - pluginManager.register(nostrFeedModule, appConfig.modules['nostr-feed']) - ) - } - - // Register chat module - if (appConfig.modules.chat.enabled) { - moduleRegistrations.push( - pluginManager.register(chatModule, appConfig.modules.chat) - ) - } - - // Register activities module (events + ticketing) - if (appConfig.modules.activities?.enabled) { - moduleRegistrations.push( - pluginManager.register(activitiesModule, appConfig.modules.activities) - ) - } - - // Register market module - if (appConfig.modules.market.enabled) { - moduleRegistrations.push( - pluginManager.register(marketModule, appConfig.modules.market) - ) - } - - // Register wallet module - if (appConfig.modules.wallet?.enabled) { - moduleRegistrations.push( - pluginManager.register(walletModule, appConfig.modules.wallet) - ) - } - - // Register expenses module - if (appConfig.modules.expenses?.enabled) { - moduleRegistrations.push( - pluginManager.register(expensesModule, appConfig.modules.expenses) - ) - } - - // Register forum module - if (appConfig.modules.forum?.enabled) { - moduleRegistrations.push( - pluginManager.register(forumModule, appConfig.modules.forum) - ) - } - - // Register tasks module - if (appConfig.modules.tasks?.enabled) { - moduleRegistrations.push( - pluginManager.register(tasksModule, appConfig.modules.tasks) - ) - } - - // Wait for all modules to register await Promise.all(moduleRegistrations) - - // Install all enabled modules await pluginManager.installAll() - // Initialize auth before setting up router guards const { auth } = await import('@/composables/useAuthService') await auth.initialize() console.log('Auth initialized, isAuthenticated:', auth.isAuthenticated.value) - // Set up auth guard router.beforeEach(async (to, _from, next) => { - // Default to requiring auth unless explicitly set to false - const requiresAuth = to.meta.requiresAuth !== false - + const requiresAuth = to.meta.requiresAuth === true + if (requiresAuth && !auth.isAuthenticated.value) { - console.log(`Auth guard: User not authenticated, redirecting from ${to.path} to login`) next('/login') } else if (to.path === '/login' && auth.isAuthenticated.value) { - console.log('Auth guard: User already authenticated, redirecting to home') next('/') } else { - console.log(`Auth guard: Allowing navigation to ${to.path} (requiresAuth: ${requiresAuth}, authenticated: ${auth.isAuthenticated.value})`) next() } }) - // Check initial route and redirect if needed - if (!auth.isAuthenticated.value) { - const currentRoute = router.currentRoute.value - const requiresAuth = currentRoute.meta.requiresAuth !== false - if (requiresAuth) { - console.log('Initial route requires auth but user not authenticated, redirecting to login') - await router.push('/login') - } - } - - // Global error handling app.config.errorHandler = (err, _vm, info) => { console.error('Global error:', err, info) eventBus.emit('app:error', { error: err, info }, 'app') } - // Development helpers if (appConfig.features.developmentMode) { - // Expose debugging helpers globally ;(window as any).__pluginManager = pluginManager ;(window as any).__eventBus = eventBus ;(window as any).__container = container - - console.log('🔧 Development mode enabled') - console.log('Available globals: __pluginManager, __eventBus, __container') } - console.log('✅ Application initialized successfully') - + console.log('✅ AIO hub initialized') return { app, router } } -/** - * Start the application - */ export async function startApp() { try { const { app } = await createAppInstance() - - // Mount the app app.mount('#app') - - console.log('🎉 Application started!') - - // Emit app started event + console.log('🎉 AIO hub started!') eventBus.emit('app:started', {}, 'app') - } catch (error) { - console.error('💥 Failed to start application:', error) - - // Show error to user + console.error('💥 Failed to start AIO hub:', error) document.getElementById('app')!.innerHTML = `
-

Application Failed to Start

+

AIO hub failed to start

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

Please refresh the page or contact support.

` } -} \ No newline at end of file +} diff --git a/src/pages/Hub.vue b/src/pages/Hub.vue new file mode 100644 index 0000000..75943c6 --- /dev/null +++ b/src/pages/Hub.vue @@ -0,0 +1,194 @@ + + +