mirror of
https://github.com/DocNR/POWR.git
synced 2025-05-14 10:15:52 +00:00
616 lines
19 KiB
Markdown
616 lines
19 KiB
Markdown
# 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<void> {
|
|
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<void> {
|
|
await this.db.runAsync(
|
|
'UPDATE templates SET is_archived = ? WHERE id = ?',
|
|
[archive ? 1 : 0, id]
|
|
);
|
|
}
|
|
|
|
async removeFromLibrary(id: string): Promise<void> {
|
|
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<void> {
|
|
// 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<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);
|
|
// 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<string, any>, 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<string, any>;
|
|
getDefaultParameters(): Record<string, any>;
|
|
formatTemplateConfig(config: Record<string, any>): string[][];
|
|
}
|
|
|
|
// Example implementation for EMOM workouts
|
|
class EMOMWorkoutProcessor implements WorkoutTypeProcessor {
|
|
parseTemplateConfig(tags: string[][]): Record<string, any> {
|
|
const config: Record<string, any> = {
|
|
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<string, any> {
|
|
return {
|
|
rounds: 10,
|
|
interval: 60,
|
|
rest: 0
|
|
};
|
|
}
|
|
|
|
formatTemplateConfig(config: Record<string, any>): 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<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;
|
|
|
|
// Get processor version
|
|
getVersion(): string;
|
|
}
|
|
|
|
// 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. 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<void>;
|
|
down(db: SQLiteDatabase): Promise<void>;
|
|
}
|
|
|
|
// 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<void> {
|
|
// 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<number> {
|
|
// Implementation
|
|
return 0;
|
|
}
|
|
|
|
private async updateVersion(db: SQLiteDatabase, version: number): Promise<void> {
|
|
// 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. |