web-app/CLAUDE.md

17 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Development Commands

Development

  • npm run dev - Start development server with Vite (includes --host flag)
  • npm run build - Build for production (includes TypeScript check with vue-tsc -b)
  • npm run preview - Preview production build locally
  • npm run analyze - Build with bundle analysis (opens visualization)

Electron Development

  • npm run electron:dev - Run both Vite dev server and Electron concurrently
  • npm run electron:build - Full build and package for Electron
  • npm run start - Start Electron using Forge
  • npm run package - Package Electron app with Forge
  • npm run make - Create distributables with Electron Forge

Architecture Overview

This is a modular Vue 3 + TypeScript + Vite application with Electron support, featuring a Nostr protocol client and Lightning Network integration for events/ticketing.

Modular Architecture

The application uses a plugin-based modular architecture with dependency injection for service management:

Core Modules:

  • Base Module (src/modules/base/) - Core infrastructure (Nostr, Auth, PWA)
  • Nostr Feed Module (src/modules/nostr-feed/) - Social feed functionality
  • Chat Module (src/modules/chat/) - Encrypted Nostr chat
  • Events Module (src/modules/events/) - Event ticketing with Lightning payments
  • Market Module (src/modules/market/) - Nostr marketplace functionality

Module Configuration:

  • Modules are configured in src/app.config.ts
  • Each module can be enabled/disabled and configured independently
  • Modules have dependencies (e.g., all modules depend on 'base')

Plugin Manager:

  • src/core/plugin-manager.ts handles module lifecycle
  • Registers, installs, and manages module dependencies
  • Handles route registration from modules

Dependency Injection Pattern

CRITICAL: Always use the dependency injection pattern for accessing shared services:

Service Registration (Base Module):

// src/modules/base/index.ts
import { container, SERVICE_TOKENS } from '@/core/di-container'

container.provide(SERVICE_TOKENS.RELAY_HUB, relayHub)
container.provide(SERVICE_TOKENS.AUTH_SERVICE, auth)

Service Consumption (Other Modules):

// In any module's composables or services
import { injectService, SERVICE_TOKENS } from '@/core/di-container'

const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE)

NEVER do this:

// DON'T import services directly - breaks modular architecture
import { relayHubComposable } from '@/composables/useRelayHub'

Always do this:

// DO use dependency injection for loose coupling
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)

Available Services:

  • SERVICE_TOKENS.RELAY_HUB - Centralized Nostr relay management
  • SERVICE_TOKENS.AUTH_SERVICE - Authentication services
  • SERVICE_TOKENS.VISIBILITY_SERVICE - App visibility and connection management

Core Stack:

  • Vue 3 with Composition API (<script setup> style)
  • TypeScript throughout
  • Vite build system with PWA support
  • Electron for desktop app packaging
  • Pinia for state management
  • Vue Router for navigation
  • TailwindCSS v4 with Shadcn/ui components
  • Vue-i18n for internationalization

Key Features:

  • Nostr protocol client for decentralized social networking
  • Lightning Network integration for event ticketing
  • PWA capabilities with service worker
  • Theme switching and language switching
  • Real-time connection status monitoring

Directory Structure:

  • src/components/ - Vue components organized by feature
    • ui/ - Shadcn/ui component library
    • nostr/ - Nostr-specific components
    • events/ - Event/ticketing components
    • layout/ - App layout components
  • src/composables/ - Vue composables for reusable logic
  • src/stores/ - Pinia stores for state management
  • src/lib/ - Core business logic
    • nostr/ - Nostr client implementation
    • api/ - API integrations
    • types/ - TypeScript type definitions
  • src/pages/ - Route pages
  • electron/ - Electron main process code

Nostr Integration: The app connects to Nostr relays using a custom NostrClient class built on nostr-tools. Key files:

  • src/lib/nostr/client.ts - Core Nostr client implementation
  • src/composables/useNostr.ts - Vue composable for Nostr connection management
  • src/stores/nostr.ts - Pinia store for Nostr state

Development Guidelines

Modular Architecture Patterns

Module Structure:

src/modules/[module-name]/
├── index.ts                 # Module plugin definition
├── components/              # Module-specific components
├── composables/             # Module composables
├── services/               # Module services
├── stores/                 # Module-specific stores
├── types/                  # Module type definitions
└── views/                  # Module pages/views

Module Plugin Pattern:

export const myModule: ModulePlugin = {
  name: 'my-module',
  version: '1.0.0',
  dependencies: ['base'], // Always depend on base for core services
  
  async install(app: App, options?: { config?: MyModuleConfig }) {
    // Module installation logic
    // Register components, initialize services, etc.
  },
  
  routes: [/* module routes */],
  components: {/* exported components */},
  composables: {/* exported composables */}
}

Service Integration:

  • All modules MUST use dependency injection for shared services
  • NEVER import services directly across module boundaries
  • Base module provides core infrastructure services
  • Modules can register their own services in the DI container

⚠️ CRITICAL - WebSocket Connection Management:

  • ALWAYS integrate with VisibilityService for any module that uses WebSocket connections
  • All services extending BaseService have automatic access to this.visibilityService
  • Register visibility callbacks during service initialization: this.visibilityService.registerService(name, onResume, onPause)
  • Implement proper connection recovery in onResume() handler (check health, reconnect if needed, restore subscriptions)
  • Implement battery-conscious pausing in onPause() handler (stop heartbeats, queue operations)
  • Mobile browsers suspend WebSocket connections when app loses visibility - visibility management is essential for reliable real-time features
  • See docs/VisibilityService.md and docs/VisibilityService-Integration.md for comprehensive integration guides
  • Future modules will likely ALL depend on WebSocket connections - plan for visibility management from the start

Centralized Infrastructure

Nostr Relay Management:

  • Single RelayHub manages all Nostr connections
  • All modules use the same relay configuration from VITE_NOSTR_RELAYS
  • No module should create separate relay connections

Authentication:

  • Centralized auth service handles all authentication
  • Modules access auth state through dependency injection
  • Router guards use the shared auth service

Configuration:

  • Environment variables prefixed with VITE_
  • Module configs in src/app.config.ts
  • Centralized config parsing and validation

Form Implementation Standards

CRITICAL: Always use Shadcn/UI Form Components with vee-validate

All forms in the application MUST follow the official Shadcn Vue form implementation pattern:

Required Form Setup:

import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import {
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/components/ui/form'

// 1. Define Zod schema for validation
const formSchema = toTypedSchema(z.object({
  name: z.string().min(1, "Name is required").max(100, "Name too long"),
  email: z.string().email("Invalid email address").optional(),
  items: z.array(z.string()).min(1, "Select at least one item"),
}))

// 2. Set up form with vee-validate
const form = useForm({
  validationSchema: formSchema,
  initialValues: {
    name: '',
    email: '',
    items: []
  }
})

// 3. Destructure form methods
const { setFieldValue, resetForm, values, meta } = form

// 4. Create form validation computed
const isFormValid = computed(() => meta.value.valid)

// 5. Create submit handler with form.handleSubmit
const onSubmit = form.handleSubmit(async (values) => {
  console.log('Form submitted:', values)
  // Handle form submission logic
})

Required Form Template Structure:

<template>
  <!-- form.handleSubmit automatically prevents default submission -->
  <form @submit="onSubmit" class="space-y-6">
    <!-- Text Input Field -->
    <FormField v-slot="{ componentField }" name="name">
      <FormItem>
        <FormLabel>Name *</FormLabel>
        <FormControl>
          <Input 
            placeholder="Enter name"
            v-bind="componentField"
          />
        </FormControl>
        <FormDescription>Enter your full name</FormDescription>
        <FormMessage />
      </FormItem>
    </FormField>

    <!-- Multiple Checkbox Selection -->
    <FormField name="items">
      <FormItem>
        <div class="mb-4">
          <FormLabel class="text-base">Items *</FormLabel>
          <FormDescription>Select one or more items</FormDescription>
        </div>
        <div v-for="item in availableItems" :key="item.id">
          <FormField
            v-slot="{ value, handleChange }"
            type="checkbox"
            :value="item.id"
            :unchecked-value="false"
            name="items"
          >
            <FormItem class="flex flex-row items-start space-x-3 space-y-0">
              <FormControl>
                <Checkbox
                  :model-value="value.includes(item.id)"
                  @update:model-value="handleChange"
                />
              </FormControl>
              <FormLabel class="font-normal">{{ item.name }}</FormLabel>
            </FormItem>
          </FormField>
        </div>
        <FormMessage />
      </FormItem>
    </FormField>

    <!-- Submit Button -->
    <Button 
      type="submit"
      :disabled="isLoading || !isFormValid"
    >
      {{ isLoading ? 'Submitting...' : 'Submit' }}
    </Button>
  </form>
</template>

Key Form Requirements:

  • Form validation: Use @submit="onSubmit" - form.handleSubmit automatically prevents page refresh
  • Button state: Disable submit button with !isFormValid until all required fields are valid
  • Error display: Use <FormMessage /> for automatic error display
  • Field binding: Use v-bind="componentField" for proper form field integration
  • Checkbox arrays: Use nested FormField pattern for multiple checkbox selection
  • Type safety: Zod schema provides full TypeScript type safety

NEVER do this:

<!-- Wrong: Manual form handling without vee-validate -->
<form @submit.prevent="handleSubmit">
  
<!-- Wrong: Direct v-model bypasses form validation -->
<Input v-model="myValue" />

<!-- Wrong: Manual validation instead of using meta.valid -->
<Button :disabled="!name || !email">Submit</Button>

ALWAYS do this:

<!-- Correct: Uses form.handleSubmit for proper form handling -->
<form @submit="onSubmit">
  
<!-- Correct: Uses FormField with componentField binding -->
<FormField v-slot="{ componentField }" name="fieldName">
  <FormControl>
    <Input v-bind="componentField" />
  </FormControl>
</FormField>

<!-- Correct: Uses form meta for validation state -->
<Button :disabled="!isFormValid">Submit</Button>

Module Development Best Practices

Module Structure Requirements:

  1. Independence: Modules must be independent of each other - no direct imports between modules
  2. Base Dependency: All modules should depend on 'base' module for core infrastructure
  3. Service Pattern: All services should extend BaseService for standardized initialization
  4. API Isolation: Module-specific API calls must be in the module's services folder
  5. Dependency Injection: Cross-module communication only through DI container

Required Module Structure:

src/modules/[module-name]/
├── index.ts                 # Module plugin definition (REQUIRED)
├── components/              # Module-specific components
├── composables/             # Module composables (use DI for services)
├── services/                # Module services (extend BaseService)
│   ├── [module]Service.ts  # Core module service
│   └── [module]API.ts       # LNbits API integration
├── stores/                  # Module-specific Pinia stores
├── types/                   # Module type definitions
└── views/                   # Module pages/views

Service Implementation Pattern:

// Always extend BaseService for services
export class MyModuleService extends BaseService {
  protected readonly metadata = {
    name: 'MyModuleService',
    version: '1.0.0',
    dependencies: [] // List service dependencies
  }

  protected async onInitialize(): Promise<void> {
    // Service-specific initialization
  }
}

// API services for LNbits integration
export class MyModuleAPI extends BaseService {
  private baseUrl: string
  
  constructor() {
    super()
    const config = appConfig.modules.myModule.config
    this.baseUrl = config.apiConfig.baseUrl
  }
  
  // API methods here
}

Module Plugin Pattern:

export const myModule: ModulePlugin = {
  name: 'my-module',
  version: '1.0.0',
  dependencies: ['base'], // Always depend on base
  
  async install(app: App, options?: { config?: MyModuleConfig }) {
    // 1. Create service instances
    const myService = new MyModuleService()
    const myAPI = new MyModuleAPI()
    
    // 2. Register in DI container
    container.provide(SERVICE_TOKENS.MY_SERVICE, myService)
    container.provide(SERVICE_TOKENS.MY_API, myAPI)
    
    // 3. Initialize services
    await myService.initialize({
      waitForDependencies: true,
      maxRetries: 3
    })
    
    // 4. Register components if needed
    app.component('MyComponent', MyComponent)
  }
}

Nostr Integration Rules:

  1. NEVER create separate relay connections - always use the central RelayHub
  2. Access RelayHub through DI: const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
  3. Use RelayHub methods for all Nostr operations (subscribe, publish, etc.)
  4. Event kinds should be module-specific and follow NIP specifications

LNbits API Integration:

  1. Create module-specific API service in services/[module]API.ts
  2. Extend BaseService for automatic dependency management
  3. Use authentication headers: X-Api-Key: walletKey
  4. Base URL from config: Use appConfig.modules.[module].config.apiConfig.baseUrl
  5. Error handling: Implement proper error handling with user feedback

Composables Best Practices:

export function useMyModule() {
  // Always use DI for services
  const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
  const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE)
  const myAPI = injectService(SERVICE_TOKENS.MY_API)
  
  // Never import services directly
  // ❌ import { relayHub } from '@/modules/base/nostr/relay-hub'
  // ✅ const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
}

WebSocket & Visibility Management:

  • Services with WebSocket connections MUST integrate with VisibilityService
  • Register visibility callbacks: this.visibilityService.registerService(name, onResume, onPause)
  • Handle connection recovery in onResume() callback
  • Implement battery-conscious pausing in onPause() callback

Code Conventions:

  • Use TypeScript interfaces over types for extendability
  • Prefer functional and declarative patterns over classes (except for services)
  • Use Vue Composition API with <script setup> syntax
  • Follow naming convention: lowercase-with-dashes for directories
  • Leverage VueUse functions for enhanced reactivity
  • Implement lazy loading for non-critical components
  • Optimize images using WebP format with lazy loading
  • ALWAYS use dependency injection for cross-module service access
  • ALWAYS use Shadcn Form components for all form implementations
  • ALWAYS extend BaseService for module services
  • NEVER create direct dependencies between modules

Build Configuration:

  • Vite config includes PWA, image optimization, and bundle analysis
  • Manual chunking strategy for vendor libraries (vue-vendor, ui-vendor, shadcn)
  • Electron Forge configured for cross-platform packaging
  • TailwindCSS v4 integration via Vite plugin

Environment:

  • Nostr relay configuration via VITE_NOSTR_RELAYS environment variable
  • PWA manifest configured for standalone app experience
  • Service worker with automatic updates every hour