Updates project documentation

Refines project documentation to reflect recent architectural changes and coding standards.

Adds detailed explanations of the BaseService pattern, module structure, and JavaScript best practices to enhance developer understanding and consistency.

Clarifies CSS styling guidelines, emphasizing semantic classes for theme-aware styling.

Includes critical bug prevention techniques related to JavaScript falsy values and correct usage of the nullish coalescing operator.

Updates build configuration details, environment variable requirements, and mobile browser workaround strategies.
This commit is contained in:
padreug 2025-11-07 14:35:38 +01:00
parent 1a38c92db1
commit b286a0315d

484
CLAUDE.md
View file

@ -26,7 +26,7 @@ This is a modular Vue 3 + TypeScript + Vite application with Electron support, f
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)
- **Base Module** (`src/modules/base/`) - Core infrastructure (Nostr, Auth, PWA, Image Upload)
- **Wallet Module** (`src/modules/wallet/`) - Lightning wallet management with real-time balance updates
- **Nostr Feed Module** (`src/modules/nostr-feed/`) - Social feed functionality
- **Chat Module** (`src/modules/chat/`) - Encrypted Nostr chat
@ -90,6 +90,12 @@ const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
- `SERVICE_TOKENS.VISIBILITY_SERVICE` - App visibility and connection management
- `SERVICE_TOKENS.WALLET_SERVICE` - Wallet operations (send, receive, transactions)
- `SERVICE_TOKENS.WALLET_WEBSOCKET_SERVICE` - Real-time wallet balance updates via WebSocket
- `SERVICE_TOKENS.STORAGE_SERVICE` - Local storage management
- `SERVICE_TOKENS.TOAST_SERVICE` - Toast notification system
- `SERVICE_TOKENS.INVOICE_SERVICE` - Lightning invoice creation and management
- `SERVICE_TOKENS.LNBITS_API` - LNbits API client
- `SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE` - Image upload to pictrs server
- `SERVICE_TOKENS.NOSTR_METADATA_SERVICE` - Nostr user metadata (NIP-01 kind 0)
**Core Stack:**
- Vue 3 with Composition API (`<script setup>` style)
@ -122,6 +128,8 @@ const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
- `api/` - API integrations
- `types/` - TypeScript type definitions
- `src/pages/` - Route pages
- `src/modules/` - Modular feature implementations
- `src/core/` - Core infrastructure (DI, BaseService, plugin manager)
- `electron/` - Electron main process code
**Lightning Wallet Integration:**
@ -143,8 +151,10 @@ The app integrates with LNbits for Lightning Network wallet functionality with r
```typescript
websocket: {
enabled: true, // Enable/disable WebSocket functionality
reconnectDelay: 1000, // Initial reconnection delay
maxReconnectAttempts: 5 // Maximum reconnection attempts
reconnectDelay: 2000, // Initial reconnection delay (ms)
maxReconnectAttempts: 3, // Maximum reconnection attempts
fallbackToPolling: true, // Enable polling fallback when WebSocket fails
pollingInterval: 10000 // Polling interval (ms)
}
```
@ -229,6 +239,88 @@ export const myModule: ModulePlugin = {
- Module configs in `src/app.config.ts`
- Centralized config parsing and validation
### **BaseService Pattern**
All services MUST extend `BaseService` (`src/core/base/BaseService.ts`) for standardized initialization and dependency management:
**Service Implementation Pattern:**
```typescript
import { BaseService } from '@/core/base/BaseService'
export class MyService extends BaseService {
// 1. REQUIRED: Declare metadata with dependencies
protected readonly metadata = {
name: 'MyService',
version: '1.0.0',
dependencies: ['AuthService', 'RelayHub', 'VisibilityService']
}
// 2. REQUIRED: Implement onInitialize
protected async onInitialize(): Promise<void> {
// Dependencies are auto-injected based on metadata.dependencies
// Available: this.authService, this.relayHub, this.visibilityService, etc.
// Register with VisibilityService if using WebSockets
if (this.visibilityService) {
this.visibilityService.registerService(
this.metadata.name,
this.onResume.bind(this),
this.onPause.bind(this)
)
}
// Your initialization logic
await this.setupConnections()
}
// 3. Implement visibility handlers for WebSocket services
private async onResume(): Promise<void> {
// Reconnect and restore state when app becomes visible
}
private async onPause(): Promise<void> {
// Pause operations when app loses visibility
}
// 4. Optional: Cleanup logic
protected async onDispose(): Promise<void> {
// Cleanup connections, subscriptions, etc.
}
}
```
**BaseService Features:**
- **Automatic dependency injection** based on `metadata.dependencies`
- **Retry logic** with configurable retries and delays
- **Reactive state** via `isInitialized`, `isInitializing`, `initError`
- **Event emission** for service lifecycle events
- **Error handling** with consistent logging
- **Debug helpers** for development
**Service Initialization:**
```typescript
// In module's index.ts
const myService = new MyService()
container.provide(SERVICE_TOKENS.MY_SERVICE, myService)
// Initialize with options
await myService.initialize({
waitForDependencies: true, // Wait for dependencies before initializing
maxRetries: 3, // Retry on failure
retryDelay: 1000 // Delay between retries (ms)
})
```
**Available Dependencies:**
When you list these in `metadata.dependencies`, they'll be auto-injected:
- `'RelayHub'``this.relayHub`
- `'AuthService'``this.authService`
- `'VisibilityService'``this.visibilityService`
- `'StorageService'``this.storageService`
- `'ToastService'``this.toastService`
- `'LnbitsAPI'``this.lnbitsAPI`
### **Form Implementation Standards**
**CRITICAL: Always use Shadcn/UI Form Components with vee-validate**
@ -382,35 +474,44 @@ For Shadcn/ui Checkbox components, you MUST use the correct Vue.js binding patte
- ✅ **Force Re-render**: Use dynamic `:key` if checkbox doesn't reflect initial form values
- ❌ **Don't Mix**: Never mix checked/model-value patterns - they have different behaviors
**Reference**: [Vue.js Forms Documentation](https://vuejs.org/guide/essentials/forms.html)
### **CSS and Styling Guidelines**
**❌ NEVER do this:**
```vue
<!-- Wrong: Manual form handling without vee-validate -->
<form @submit.prevent="handleSubmit">
**CRITICAL: Always use semantic, theme-aware CSS classes**
<!-- Wrong: Direct v-model bypasses form validation -->
<Input v-model="myValue" />
- ✅ **Use semantic classes** that automatically adapt to light/dark themes
- ❌ **Never use hard-coded colors** like `bg-white`, `text-gray-500`, `border-blue-500`
<!-- Wrong: Manual validation instead of using meta.valid -->
<Button :disabled="!name || !email">Submit</Button>
**Preferred Semantic Classes:**
```css
/* Background Colors */
bg-background /* Instead of bg-white */
bg-card /* Instead of bg-gray-50 */
bg-muted /* Instead of bg-gray-100 */
/* Text Colors */
text-foreground /* Instead of text-gray-900 */
text-muted-foreground /* Instead of text-gray-600 */
text-primary /* For primary theme color */
text-accent /* For accent theme color */
/* Borders */
border-border /* Instead of border-gray-200 */
border-input /* Instead of border-gray-300 */
/* Focus States */
focus:ring-ring /* Instead of focus:ring-blue-500 */
focus:border-ring /* Instead of focus:border-blue-500 */
/* Opacity Modifiers */
bg-primary/10 /* For subtle variations */
text-muted-foreground/70 /* For transparency */
```
**✅ ALWAYS do this:**
```vue
<!-- 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>
```
**Why Semantic Classes:**
- Ensures components work in both light and dark themes
- Maintains consistency with Shadcn/ui component library
- Easier to maintain and update theme colors globally
- Better accessibility
### **Vue Reactivity Best Practices**
@ -463,23 +564,32 @@ createdObject.value = Object.assign({}, apiResponse)
- ✅ Input components showing external data
- ✅ Any scenario where template doesn't update after data changes
**Example from Wallet Module:**
### **⚠️ CRITICAL: JavaScript Falsy Value Bug Prevention**
**ALWAYS use nullish coalescing (`??`) instead of logical OR (`||`) for numeric defaults:**
```typescript
// Service returns complex invoice object
const invoice = await walletService.createInvoice(data)
// ❌ WRONG: Treats 0 as falsy, defaults to 1 even when quantity is validly 0
quantity: productData.quantity || 1
// Force reactivity for template updates
createdInvoice.value = Object.assign({}, invoice)
// ✅ CORRECT: Only defaults to 1 when quantity is null or undefined
quantity: productData.quantity ?? 1
```
```vue
<!-- Template with forced reactivity -->
<Input
:key="`bolt11-${createdInvoice?.payment_hash}`"
:model-value="createdInvoice?.payment_request || ''"
readonly
/>
```
**Why this matters:**
- JavaScript falsy values include: `false`, `0`, `""`, `null`, `undefined`, `NaN`
- Using `||` for defaults will incorrectly override valid `0` values
- This caused a critical bug where products with quantity `0` displayed as quantity `1`
- The `??` operator only triggers for `null` and `undefined`, preserving valid `0` values
**Common scenarios where this bug occurs:**
- Product quantities, prices, counters (any numeric value where 0 is valid)
- Boolean flags where `false` is a valid state
- Empty strings that should be preserved vs. undefined strings
**Rule of thumb:**
- Use `||` only when `0`, `false`, or `""` should trigger the default
- Use `??` when only `null`/`undefined` should trigger the default (most cases)
### **Module Development Best Practices**
@ -530,166 +640,6 @@ Before considering any module complete, verify ALL items:
- [ ] Configuration is properly loaded
- [ ] Module can be disabled via config
**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:**
**⚠️ CRITICAL SERVICE REQUIREMENTS - MUST FOLLOW EXACTLY:**
```typescript
// ✅ CORRECT: Proper BaseService implementation
export class MyModuleService extends BaseService {
// 1. REQUIRED: Declare metadata with dependencies
protected readonly metadata = {
name: 'MyModuleService',
version: '1.0.0',
dependencies: ['PaymentService', 'AuthService'] // List ALL service dependencies by name
}
// 2. REQUIRED: DO NOT manually inject services in onInitialize
protected async onInitialize(): Promise<void> {
// ❌ WRONG: Manual injection
// this.paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE)
// ✅ CORRECT: BaseService auto-injects based on metadata.dependencies
// this.paymentService is already available here!
// 3. REQUIRED: Register with VisibilityService if you have ANY real-time features
if (this.hasRealTimeFeatures()) {
this.visibilityService.registerService(
this.metadata.name,
this.onResume.bind(this),
this.onPause.bind(this)
)
}
// 4. Initialize your module-specific logic
await this.loadInitialData()
}
// 5. REQUIRED: Implement visibility handlers for connection management
private async onResume(): Promise<void> {
// Restore connections, refresh data when app becomes visible
await this.checkConnectionHealth()
await this.refreshData()
}
private async onPause(): Promise<void> {
// Pause expensive operations for battery efficiency
this.pausePolling()
}
private hasRealTimeFeatures(): boolean {
// Return true if your service uses WebSockets, polling, or real-time updates
return true
}
}
// API services for LNbits integration
export class MyModuleAPI extends BaseService {
private baseUrl: string
constructor() {
super()
// ❌ WRONG: Direct config import
// import { config } from '@/lib/config'
// ✅ CORRECT: Use module configuration
const moduleConfig = appConfig.modules.myModule.config
this.baseUrl = moduleConfig.apiConfig.baseUrl
}
// API methods here
}
```
**❌ COMMON MISTAKES TO AVOID:**
1. **Manual service injection** in onInitialize - BaseService handles this
2. **Direct config imports** - Always use module configuration
3. **Missing metadata.dependencies** - Breaks automatic dependency injection
4. **No VisibilityService integration** - Causes connection issues on mobile
5. **Not using proper initialization options** - Miss dependency waiting
**Module Plugin Pattern:**
**⚠️ CRITICAL MODULE INSTALLATION REQUIREMENTS:**
```typescript
export const myModule: ModulePlugin = {
name: 'my-module',
version: '1.0.0',
dependencies: ['base'], // ALWAYS depend on 'base' for core infrastructure
async install(app: App, options?: { config?: MyModuleConfig }) {
// 1. REQUIRED: Create service instances
const myService = new MyModuleService()
const myAPI = new MyModuleAPI()
// 2. REQUIRED: Register in DI container BEFORE initialization
container.provide(SERVICE_TOKENS.MY_SERVICE, myService)
container.provide(SERVICE_TOKENS.MY_API, myAPI)
// 3. CRITICAL: Initialize services with proper options
await myService.initialize({
waitForDependencies: true, // REQUIRED: Wait for dependencies
maxRetries: 3, // RECOMMENDED: Retry on failure
timeout: 5000 // OPTIONAL: Timeout for initialization
})
// Initialize API service if it needs initialization
if (myAPI.initialize) {
await myAPI.initialize({
waitForDependencies: true,
maxRetries: 3
})
}
// 4. Register components AFTER services are initialized
app.component('MyComponent', MyComponent)
// 5. OPTIONAL: Export for testing/debugging
return {
service: myService,
api: myAPI
}
}
}
```
**MODULE CONFIGURATION IN app.config.ts:**
```typescript
// REQUIRED: Add module configuration
export default {
modules: {
'my-module': {
enabled: true,
config: {
apiConfig: {
baseUrl: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000'
},
// Module-specific configuration
features: {
realTimeUpdates: true,
offlineSupport: false
}
}
}
}
}
```
**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)`
@ -735,44 +685,34 @@ export function useMyModule() {
- **ALWAYS use Shadcn Form components for all form implementations**
- **ALWAYS extend BaseService for module services**
- **NEVER create direct dependencies between modules**
- **ALWAYS use semantic CSS classes, never hard-coded colors**
### **⚠️ CRITICAL: JavaScript Falsy Value Bug Prevention**
**ALWAYS use nullish coalescing (`??`) instead of logical OR (`||`) for numeric defaults:**
```typescript
// ❌ WRONG: Treats 0 as falsy, defaults to 1 even when quantity is validly 0
quantity: productData.quantity || 1
// ✅ CORRECT: Only defaults to 1 when quantity is null or undefined
quantity: productData.quantity ?? 1
```
**Why this matters:**
- JavaScript falsy values include: `false`, `0`, `""`, `null`, `undefined`, `NaN`
- Using `||` for defaults will incorrectly override valid `0` values
- This caused a critical bug where products with quantity `0` displayed as quantity `1`
- The `??` operator only triggers for `null` and `undefined`, preserving valid `0` values
**Common scenarios where this bug occurs:**
- Product quantities, prices, counters (any numeric value where 0 is valid)
- Boolean flags where `false` is a valid state
- Empty strings that should be preserved vs. undefined strings
**Rule of thumb:**
- Use `||` only when `0`, `false`, or `""` should trigger the default
- Use `??` when only `null`/`undefined` should trigger the default (most cases)
**Build Configuration:**
### **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
### **Environment Variables:**
Required environment variables in `.env`:
```bash
# LNbits server URL for Lightning wallet functionality
VITE_LNBITS_BASE_URL=http://localhost:5000
# Nostr relay configuration (JSON array)
VITE_NOSTR_RELAYS='["wss://relay1.example.com","wss://relay2.example.com"]'
# Image upload server (pictrs)
VITE_PICTRS_BASE_URL=https://img.mydomain.com
# Admin public keys for feed moderation (JSON array)
VITE_ADMIN_PUBKEYS='["pubkey1","pubkey2"]'
# Optional: Disable WebSocket if needed
VITE_WEBSOCKET_ENABLED=true
```
## Mobile Browser File Input & Form Refresh Issues
@ -906,86 +846,6 @@ window.addEventListener('beforeunload', blockNavigation)
<form @submit.prevent="handleSubmit">
```
### **Android 14/15 Camera Workarounds**
**Non-Standard MIME Type Workaround:**
```html
<!-- Add non-standard MIME type to force camera access -->
<input type="file" accept="image/*,android/allowCamera" capture="environment" />
```
**Plain File Input Fallback:**
```html
<!-- Fallback: Plain file input shows both camera and gallery options -->
<input type="file" accept="image/*" />
```
### **Industry-Standard Patterns**
**1. Page Visibility API (Primary Solution):**
```javascript
// Modern browsers: Use Page Visibility API instead of beforeunload
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') {
// Resume critical operations, restore connections
resumeOperations()
} else {
// Save state, pause operations for battery conservation
saveStateAndPause()
}
})
```
**2. Conditional BeforeUnload Protection:**
```javascript
// Only add beforeunload listeners when user has unsaved changes
const addFormProtection = (hasUnsavedChanges) => {
if (hasUnsavedChanges) {
window.addEventListener('beforeunload', preventUnload)
} else {
window.removeEventListener('beforeunload', preventUnload)
}
}
```
**3. Session Recovery Pattern:**
```javascript
// Save form state on visibility change
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
localStorage.setItem('formDraft', JSON.stringify(formData))
}
})
// Restore on page load
window.addEventListener('load', () => {
const draft = localStorage.getItem('formDraft')
if (draft) restoreFormData(JSON.parse(draft))
})
```
### **Testing & Debugging**
**Reproduction Steps:**
1. Open form with file upload on mobile device
2. Select camera input during image upload operations
3. Turn screen off/on during upload process
4. Switch between apps during file selection
5. Low memory conditions during camera usage
**Success Indicators:**
- User sees confirmation dialog instead of losing form data
- Console warnings show visibility change detection working
- Form state preservation during app switching
- Camera input properly separates from gallery input
**Debug Console Messages:**
```javascript
// Look for these defensive programming console messages
console.warn('Form submission blocked during file upload')
console.warn('Visibility change detected while form is open')
```
### **Key Takeaways**
1. **This is a systemic mobile browser issue**, not a bug in our application code