# Updated POWR Pack Integration Plan ## Current Status Assessment Based on the current implementation of POWR Packs, we've identified several issues that need to be addressed: 1. **Missing Template-Exercise Relationships**: Templates are being imported but not properly linked to their associated exercises 2. **Parameter Extraction Issues**: The system isn't correctly parsing parameters from exercise references 3. **Lack of Future Extensibility**: The current approach doesn't adequately support future changes to the NIP-4e specification 4. **Template Management**: Tools for template archiving and deletion are incomplete ## Implementation Plan This plan outlines both immediate fixes and longer-term improvements for a more extensible architecture. ### Phase 1: Critical Fixes (Immediate) #### 1. Fix Template-Exercise Relationship **Problem**: Templates are imported but show 0 exercises because the references aren't correctly matched. **Solution**: - Update `POWRPackService.ts` to correctly parse exercise references by d-tag - Improve the exercise matching logic to use the correct format (`33401:pubkey:d-tag`) - Add detailed logging for troubleshooting ```typescript // Find the corresponding imported exercise IDs const templateExerciseIds: string[] = []; const matchedRefs: string[] = []; for (const ref of exerciseRefs) { // Extract the base reference (before any parameters) const refParts = ref.split('::'); const baseRef = refParts[0]; console.log(`Looking for matching exercise for reference: ${baseRef}`); // Parse the reference format: kind:pubkey:d-tag const refSegments = baseRef.split(':'); if (refSegments.length < 3) { console.log(`Invalid reference format: ${baseRef}`); 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'); if (!dTag || e.pubkey !== refPubkey) return false; const match = dTag === refDTag; if (match) { console.log(`Found matching event: ${e.id} with d-tag: ${dTag}`); } return match; }); if (matchingEvent && exerciseIdMap.has(matchingEvent.id)) { const localExerciseId = exerciseIdMap.get(matchingEvent.id) || ''; templateExerciseIds.push(localExerciseId); matchedRefs.push(ref); // Keep the full reference including parameters console.log(`Mapped Nostr event ${matchingEvent.id} to local exercise ID ${localExerciseId}`); } else { console.log(`No matching exercise found for reference: ${baseRef}`); } } ``` #### 2. Fix Parameter Extraction in NostrIntegration.ts **Problem**: Parameter values from exercise references aren't being properly extracted. **Solution**: ```typescript async saveTemplateExercisesWithParams( templateId: string, exerciseIds: string[], exerciseRefs: string[] ): Promise { try { console.log(`Saving ${exerciseIds.length} exercise relationships for template ${templateId}`); // Create template exercise records for (let i = 0; i < exerciseIds.length; i++) { const exerciseId = exerciseIds[i]; const templateExerciseId = generateId(); const now = Date.now(); // Get the corresponding exercise reference with parameters const exerciseRef = exerciseRefs[i] || ''; // Parse the reference format: kind:pubkey:d-tag::sets:reps:weight let targetSets = null; let targetReps = null; let targetWeight = null; // Check if reference contains parameters if (exerciseRef.includes('::')) { const parts = exerciseRef.split('::'); if (parts.length > 1) { const params = parts[1].split(':'); if (params.length > 0 && params[0]) targetSets = parseInt(params[0]) || null; if (params.length > 1 && params[1]) targetReps = parseInt(params[1]) || null; if (params.length > 2 && params[2]) targetWeight = parseFloat(params[2]) || null; } } console.log(`Template exercise ${i}: ${exerciseId} with sets=${targetSets}, reps=${targetReps}, weight=${targetWeight}`); await this.db.runAsync( `INSERT INTO template_exercises (id, template_id, exercise_id, display_order, target_sets, target_reps, target_weight, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ templateExerciseId, templateId, exerciseId, i, targetSets, targetReps, targetWeight, now, now ] ); } console.log(`Successfully saved all template-exercise relationships for template ${templateId}`); } catch (error) { console.error('Error saving template exercises with parameters:', error); throw error; } } ``` #### 3. Add Template Management Functions **Problem**: Need better tools for template archiving and deletion. **Solution**: - Add an `is_archived` column to templates table - Create archive/unarchive functions - Implement safe template removal with dependency handling ```typescript // Schema update await db.execAsync(` ALTER TABLE templates ADD COLUMN is_archived BOOLEAN NOT NULL DEFAULT 0; ALTER TABLE templates ADD COLUMN author_pubkey TEXT; `); // Template management functions async archiveTemplate(id: string, archive: boolean = true): Promise { await this.db.runAsync( 'UPDATE templates SET is_archived = ? WHERE id = ?', [archive ? 1 : 0, id] ); } async removeFromLibrary(id: string): Promise { await this.db.withTransactionAsync(async () => { // Delete template-exercise relationships await this.db.runAsync( 'DELETE FROM template_exercises WHERE template_id = ?', [id] ); // Delete template await this.db.runAsync( 'DELETE FROM templates WHERE id = ?', [id] ); // Update powr_pack_items to mark as not imported await this.db.runAsync( 'UPDATE powr_pack_items SET is_imported = 0 WHERE item_id = ? AND item_type = "template"', [id] ); }); } ``` ### Phase 2: Extensibility Improvements (Short-term) #### 1. Schema Updates for Extensibility **Problem**: Schema is too rigid for future extensions to exercise parameters and workout types. **Solution**: ```typescript // Add schema update in a migration file or update schema.ts async function addExtensibilityColumns(db: SQLiteDatabase): Promise { // Add params_json to template_exercises for extensible parameters await db.execAsync(` ALTER TABLE template_exercises ADD COLUMN params_json TEXT; `); // Add workout_type_config to templates for type-specific configurations await db.execAsync(` ALTER TABLE templates ADD COLUMN workout_type_config TEXT; `); } ``` #### 2. Flexible Parameter Extraction **Problem**: Current parameter extraction is hardcoded for a limited set of parameters. **Solution**: - Create a parameter mapper service - Implement dynamic parameter extraction based on exercise format ```typescript class ExerciseParameterMapper { // Extract parameters from a Nostr reference based on exercise format static extractParameters(exerciseRef: string, formatJson?: string): Record { const parameters: Record = {}; // 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); // Fall back to default mapping below } } // 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; } // Convert parameters back to Nostr reference format static formatParameters(parameters: Record, formatJson?: string): string { if (!Object.keys(parameters).length) return ''; let paramArray: (string | number | null)[] = []; // If we have format information, use it for parameter ordering if (formatJson) { try { const format = JSON.parse(formatJson); const formatKeys = Object.keys(format).filter(key => format[key] === true); paramArray = formatKeys.map(key => parameters[key] ?? ''); } catch (error) { console.warn('Error parsing format JSON:', error); // Fall back to default format below } } // Default parameter format if no format JSON or error parsing if (!paramArray.length) { paramArray = [ parameters.target_sets ?? parameters.sets ?? '', parameters.target_reps ?? parameters.reps ?? '', parameters.target_weight ?? parameters.weight ?? '', parameters.set_type ?? '' ]; } // Trim trailing empty values while (paramArray.length > 0 && (paramArray[paramArray.length - 1] === '' || paramArray[paramArray.length - 1] === null)) { paramArray.pop(); } // If no parameters left, return empty string if (!paramArray.length) return ''; // Join parameters with colon return paramArray.join(':'); } } ``` #### 3. Workout Type-Specific Handling **Problem**: Different workout types (AMRAP, EMOM, circuit, strength) have specific data needs. **Solution**: - Create workout type processors - Implement template service enhancements for type-specific configurations ```typescript // WorkoutTypesService.ts import { WorkoutTemplate, TemplateType } from '@/types/templates'; // Factory pattern for creating workout type processors export class WorkoutTypeFactory { static createProcessor(type: TemplateType): 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(); } } } // Interface for workout type processors export interface WorkoutTypeProcessor { parseTemplateConfig(tags: string[][]): Record; getDefaultParameters(): Record; formatTemplateConfig(config: Record): string[][]; } // Example implementation for EMOM workouts class EMOMWorkoutProcessor implements WorkoutTypeProcessor { parseTemplateConfig(tags: string[][]): Record { const config: Record = { type: 'emom', rounds: 0, interval: 60, // Default 60 seconds rest: 0 }; // Extract rounds (total number of intervals) const roundsTag = tags.find(t => t[0] === 'rounds'); if (roundsTag && roundsTag.length > 1) { config.rounds = parseInt(roundsTag[1]) || 0; } // Extract interval duration const intervalTag = tags.find(t => t[0] === 'interval'); if (intervalTag && intervalTag.length > 1) { config.interval = parseInt(intervalTag[1]) || 60; } // Extract rest between rounds const restTag = tags.find(t => t[0] === 'rest_between_rounds'); if (restTag && restTag.length > 1) { config.rest = parseInt(restTag[1]) || 0; } return config; } getDefaultParameters(): Record { return { rounds: 10, interval: 60, rest: 0 }; } formatTemplateConfig(config: Record): string[][] { const tags: string[][] = []; if (config.rounds) { tags.push(['rounds', config.rounds.toString()]); } if (config.interval) { tags.push(['interval', config.interval.toString()]); } if (config.rest) { tags.push(['rest_between_rounds', config.rest.toString()]); } return tags; } } ``` ### Phase 3: Long-Term Architecture (Future) #### 1. Modular Event Processor Architecture **Problem**: Need a more adaptable system for handling evolving Nostr event schemas. **Solution**: - Create a plugin-based architecture for event processors - Implement versioning for Nostr event handling - Design a flexible mapping system between Nostr events and local database schema ```typescript // Interface for event processors interface NostrEventProcessor { // 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; // Get processor version getVersion(): string; } // Registry for event processors class EventProcessorRegistry { private processors: Map[]> = new Map(); // Register a processor for a specific kind registerProcessor(kind: number, processor: NostrEventProcessor): void { if (!this.processors.has(kind)) { this.processors.set(kind, []); } this.processors.get(kind)?.push(processor); } // Get appropriate processor for an event getProcessor(event: NostrEvent): NostrEventProcessor | 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; } } return null; } } ``` #### 2. Schema Migration System **Problem**: Database schema needs to evolve with Nostr specification changes. **Solution**: - Create a versioned migration system - Implement automatic schema updates - Track schema versions ```typescript // Migration interface interface SchemaMigration { version: number; up(db: SQLiteDatabase): Promise; down(db: SQLiteDatabase): Promise; } // Migration runner class MigrationRunner { private migrations: SchemaMigration[] = []; // Register a migration registerMigration(migration: SchemaMigration): void { this.migrations.push(migration); // Sort migrations by version this.migrations.sort((a, b) => a.version - b.version); } // Run migrations up to a specific version async migrate(db: SQLiteDatabase, targetVersion: number): Promise { // Get current version const currentVersion = await this.getCurrentVersion(db); if (currentVersion < targetVersion) { // Run UP migrations for (const migration of this.migrations) { if (migration.version > currentVersion && migration.version <= targetVersion) { await migration.up(db); await this.updateVersion(db, migration.version); } } } else if (currentVersion > targetVersion) { // Run DOWN migrations for (const migration of [...this.migrations].reverse()) { if (migration.version <= currentVersion && migration.version > targetVersion) { await migration.down(db); await this.updateVersion(db, migration.version - 1); } } } } // Helper methods private async getCurrentVersion(db: SQLiteDatabase): Promise { // Implementation return 0; } private async updateVersion(db: SQLiteDatabase, version: number): Promise { // Implementation } } ``` #### 3. Future-Proof Integration Patterns **Problem**: Need to ensure the POWR app can adapt to future Nostr specification changes. **Solution**: - Implement adapter pattern for Nostr protocol - Create abstraction layers for data synchronization - Design entity mappers for different data versions ```typescript // Adapter for Nostr protocol versions interface NostrProtocolAdapter { // Get exercise from event getExerciseFromEvent(event: NostrEvent): BaseExercise; // Get template from event getTemplateFromEvent(event: NostrEvent): WorkoutTemplate; // Get workout record from event getWorkoutFromEvent(event: NostrEvent): Workout; // Create events from local models createExerciseEvent(exercise: BaseExercise): NostrEvent; createTemplateEvent(template: WorkoutTemplate): NostrEvent; createWorkoutEvent(workout: Workout): NostrEvent; } // Versioned adapter implementation class NostrProtocolAdapterV1 implements NostrProtocolAdapter { // Implementation for first version of NIP-4e } ``` ## Testing Strategy ### Phase 1 (Immediate) 1. Create a test POWR Pack with variety of exercise types and templates 2. Test importing the pack with the updated code 3. Verify that templates contain the correct exercise relationships 4. Validate parameter extraction works correctly ### Phase 2 (Short-term) 1. Create test cases for different workout types (strength, circuit, EMOM, AMRAP) 2. Verify parameter mapping works as expected 3. Test template management functions ### Phase 3 (Long-term) 1. Create comprehensive integration tests 2. Design migration testing framework 3. Implement automated testing for different Nostr protocol versions ## Implementation Timeline ### Phase 1: Critical Fixes - **Day 1**: Fix template-exercise relationship in `POWRPackService.ts` - **Day 2**: Fix parameter extraction in `NostrIntegration.ts` - **Day 3**: Implement template management functions and schema updates - **Day 4**: Testing and bug fixes ### Phase 2: Extensibility Improvements - **Week 2**: Implement schema updates and flexible parameter extraction - **Week 3**: Develop workout type-specific processing - **Week 4**: UI enhancements and testing ### Phase 3: Long-Term Architecture - **Future**: Implement as part of broader architectural improvements ## Conclusion This updated plan addresses both the immediate issues with POWR Pack integration and lays out a path for future extensibility as the Nostr Exercise NIP evolves. By implementing these changes in phases, we can quickly fix the current template-exercise relationship problems while establishing a foundation for more sophisticated features in the future. The proposed approach balances pragmatism with future-proofing, ensuring that users can immediately benefit from POWR Packs while the system remains adaptable to changes in workout types, exercise parameters, and Nostr protocol specifications.