POWR/docs/features/powr_packs/implementation_plan.md

12 KiB

POWR Pack Implementation Plan

Last Updated: 2025-03-26
Status: Active
Related To: Workout Templates, Exercises, Nostr Integration

Purpose

This document outlines the detailed implementation plan for the POWR Pack feature, focusing on both immediate technical solutions and longer-term architectural improvements. It serves as a guide for developers implementing and extending the feature.

Current Status Assessment

Based on the current implementation of POWR Packs, several areas need attention:

  1. Template-Exercise Relationships: Templates are being imported but not properly linked to their associated exercises
  2. Parameter Extraction: The system needs improvement in parsing parameters from exercise references
  3. Future Extensibility: The current approach should support future changes to the NIP-4e specification
  4. Template Management: Tools for template archiving and deletion need enhancement

Implementation Phases

Phase 1: Core Functionality (Implemented)

The basic functionality of POWR Packs has been implemented, including:

  1. Database Schema: Tables to track imported packs and their contents
  2. POWRPackService: Service for fetching packs from Nostr and importing them
  3. Import UI: Interface for users to input naddr1 links and select content to import
  4. Management UI: Interface for viewing and deleting imported packs

Phase 2: Technical Enhancements

Schema Extensions

-- POWR Packs table
CREATE TABLE powr_packs (
  id TEXT PRIMARY KEY,
  title TEXT NOT NULL,
  description TEXT,
  author_pubkey TEXT,
  nostr_event_id TEXT,
  import_date INTEGER NOT NULL
);

-- POWR Pack items table
CREATE TABLE powr_pack_items (
  pack_id TEXT NOT NULL,
  item_id TEXT NOT NULL,
  item_type TEXT NOT NULL,
  item_order INTEGER,
  is_imported BOOLEAN NOT NULL DEFAULT 1,
  PRIMARY KEY (pack_id, item_id),
  FOREIGN KEY (pack_id) REFERENCES powr_packs(id) ON DELETE CASCADE
);

-- Template extensions
ALTER TABLE templates ADD COLUMN is_archived BOOLEAN NOT NULL DEFAULT 0;
ALTER TABLE templates ADD COLUMN author_pubkey TEXT;
ALTER TABLE templates ADD COLUMN workout_type_config TEXT;

-- Template exercise extensions
ALTER TABLE template_exercises ADD COLUMN params_json TEXT;

Template-Exercise Relationship Improvements

The implementation needs to properly handle the relationship between templates and exercises:

// Find matching exercises based on Nostr references
function matchExerciseReferences(exercises: NDKEvent[], exerciseRefs: string[]): Map<string, string> {
  const matchMap = new Map<string, string>();
  
  for (const ref of exerciseRefs) {
    // Extract the base reference (before any parameters)
    const refParts = ref.split('::');
    const baseRef = refParts[0];
    
    // Parse the reference format: kind:pubkey:d-tag
    const refSegments = baseRef.split(':');
    if (refSegments.length < 3) continue;
    
    const refKind = refSegments[0];
    const refPubkey = refSegments[1];
    const refDTag = refSegments[2];
    
    // Find the event that matches by d-tag
    const matchingEvent = exercises.find(e => {
      const dTag = findTagValue(e.tags, 'd');
      return dTag === refDTag && e.pubkey === refPubkey;
    });
    
    if (matchingEvent) {
      matchMap.set(ref, matchingEvent.id);
    }
  }
  
  return matchMap;
}

Parameter Extraction

The system needs to properly extract parameters from exercise references:

// Extract parameters from exercise reference
function extractParameters(exerciseRef: string): Record<string, any> {
  const parameters: Record<string, any> = {};
  
  // If no reference with parameters, return empty object
  if (!exerciseRef || !exerciseRef.includes('::')) {
    return parameters;
  }
  
  const [baseRef, paramString] = exerciseRef.split('::');
  if (!paramString) return parameters;
  
  const paramValues = paramString.split(':');
  
  // Map parameters to standard names
  if (paramValues.length > 0) parameters.target_sets = parseInt(paramValues[0]) || null;
  if (paramValues.length > 1) parameters.target_reps = parseInt(paramValues[1]) || null;
  if (paramValues.length > 2) parameters.target_weight = parseFloat(paramValues[2]) || null;
  if (paramValues.length > 3) parameters.set_type = paramValues[3];
  
  return parameters;
}

Template Management

Enhanced template management functions:

// Archive/unarchive a template
async function archiveTemplate(id: string, archive: boolean = true): Promise<void> {
  await db.runAsync(
    'UPDATE templates SET is_archived = ? WHERE id = ?',
    [archive ? 1 : 0, id]
  );
}

// Remove a template from the library
async function removeTemplateFromLibrary(id: string): Promise<void> {
  await db.withTransactionAsync(async () => {
    // Delete template-exercise relationships
    await db.runAsync(
      'DELETE FROM template_exercises WHERE template_id = ?',
      [id]
    );
    
    // Delete template
    await db.runAsync(
      'DELETE FROM templates WHERE id = ?',
      [id]
    );
    
    // Update powr_pack_items to mark as not imported
    await db.runAsync(
      'UPDATE powr_pack_items SET is_imported = 0 WHERE item_id = ? AND item_type = "template"',
      [id]
    );
  });
}

Phase 3: Extensibility Improvements

To support future extensions to the NIP-4e specification, the implementation should include:

1. Flexible Parameter Handling

Create a parameter mapper service that can dynamically handle different parameter formats:

class ExerciseParameterMapper {
  // Extract parameters from a Nostr reference based on exercise format
  static extractParameters(exerciseRef: string, formatJson?: string): Record<string, any> {
    const parameters: Record<string, any> = {};
    
    // If no reference with parameters, return empty object
    if (!exerciseRef || !exerciseRef.includes('::')) {
      return parameters;
    }
    
    const [baseRef, paramString] = exerciseRef.split('::');
    if (!paramString) return parameters;
    
    const paramValues = paramString.split(':');
    
    // If we have format information, use it to map parameters
    if (formatJson) {
      try {
        const format = JSON.parse(formatJson);
        const formatKeys = Object.keys(format).filter(key => format[key] === true);
        
        formatKeys.forEach((key, index) => {
          if (index < paramValues.length && paramValues[index]) {
            // Convert value to appropriate type based on parameter name
            if (key === 'weight') {
              parameters[key] = parseFloat(paramValues[index]) || null;
            } else if (['reps', 'sets', 'duration'].includes(key)) {
              parameters[key] = parseInt(paramValues[index]) || null;
            } else {
              // For other parameters, keep as string
              parameters[key] = paramValues[index];
            }
          }
        });
        
        return parameters;
      } catch (error) {
        console.warn('Error parsing format JSON:', error);
      }
    }
    
    // Default parameter mapping if no format or error parsing
    if (paramValues.length > 0) parameters.target_sets = parseInt(paramValues[0]) || null;
    if (paramValues.length > 1) parameters.target_reps = parseInt(paramValues[1]) || null;
    if (paramValues.length > 2) parameters.target_weight = parseFloat(paramValues[2]) || null;
    if (paramValues.length > 3) parameters.set_type = paramValues[3];
    
    return parameters;
  }
}

2. Workout Type-Specific Handling

Create processors for different workout types to handle their specific data needs:

// Interface for workout type processors
interface WorkoutTypeProcessor {
  parseTemplateConfig(tags: string[][]): Record<string, any>;
  getDefaultParameters(): Record<string, any>;
  formatTemplateConfig(config: Record<string, any>): string[][];
}

// Factory for creating workout type processors
class WorkoutTypeFactory {
  static createProcessor(type: string): WorkoutTypeProcessor {
    switch (type) {
      case 'strength':
        return new StrengthWorkoutProcessor();
      case 'circuit':
        return new CircuitWorkoutProcessor();
      case 'emom':
        return new EMOMWorkoutProcessor();
      case 'amrap':
        return new AMRAPWorkoutProcessor();
      default:
        return new DefaultWorkoutProcessor();
    }
  }
}

Phase 4: Future Architecture

For longer-term development, consider implementing:

1. Modular Event Processor Architecture

// Interface for event processors
interface NostrEventProcessor<T> {
  // Check if processor can handle this event
  canProcess(event: NostrEvent): boolean;
  
  // Process event to local model
  processEvent(event: NostrEvent): T;
  
  // Convert local model to event
  createEvent(model: T): NostrEvent;
}

// Registry for event processors
class EventProcessorRegistry {
  private processors: Map<number, NostrEventProcessor<any>[]> = new Map();
  
  // Register a processor for a specific kind
  registerProcessor(kind: number, processor: NostrEventProcessor<any>): void {
    if (!this.processors.has(kind)) {
      this.processors.set(kind, []);
    }
    
    this.processors.get(kind)?.push(processor);
  }
  
  // Get appropriate processor for an event
  getProcessor<T>(event: NostrEvent): NostrEventProcessor<T> | null {
    const kindProcessors = this.processors.get(event.kind);
    if (!kindProcessors) return null;
    
    // Find the first processor that can process this event
    for (const processor of kindProcessors) {
      if (processor.canProcess(event)) {
        return processor as NostrEventProcessor<T>;
      }
    }
    
    return null;
  }
}

2. Version-Aware Adapters

// Adapter for Nostr protocol versions
interface NostrProtocolAdapter {
  // Get exercise from event
  getExerciseFromEvent(event: NostrEvent): Exercise;
  
  // Get template from event
  getTemplateFromEvent(event: NostrEvent): WorkoutTemplate;
  
  // Create events from local models
  createExerciseEvent(exercise: Exercise): NostrEvent;
  createTemplateEvent(template: WorkoutTemplate): NostrEvent;
}

UI Components

Import Screen

The import screen should include:

  1. Input field for naddr1 links
  2. Pack details display (title, description, author)
  3. Selectable list of templates with thumbnails
  4. Selectable list of exercises with auto-selection based on template dependencies
  5. Import button with count of selected items

Management Screen

The management screen should include:

  1. List of imported packs with:
    • Pack title and description
    • Author information with avatar
    • Number of templates and exercises
    • Import date
    • Delete button with confirmation dialog

Social Discovery

The social tab should include a POWR Packs section with:

  1. Horizontal scrolling list of available packs
  2. Pack cards with:
    • Pack title and thumbnail
    • Author information
    • Brief description
    • "View Details" button

Testing Strategy

Unit Tests

  1. Test template-exercise relationship mapping
  2. Test parameter extraction and formatting
  3. Test template management functions
  4. Test pack importing and deletion

Integration Tests

  1. Test end-to-end importing flow with mock Nostr events
  2. Test dependency handling when selecting templates
  3. Test social discovery functionality

User Acceptance Tests

  1. Test import flow with real Nostr packs
  2. Test management interface with multiple imported packs
  3. Test error handling with invalid naddr1 links

Implementation Timeline

  1. Phase 1 (Complete): Core functionality implementation
  2. Phase 2 (Weeks 1-2): Technical enhancements
    • Fix template-exercise relationship
    • Improve parameter extraction
    • Enhance template management
  3. Phase 3 (Weeks 3-4): Extensibility improvements
    • Implement flexible parameter handling
    • Add workout type-specific processing
  4. Phase 4 (Future): Advanced architecture
    • Implement modular event processor architecture
    • Develop version-aware adapters