Compare commits

2 Commits

Author SHA1 Message Date
cee20f6bb3 fitness: improve weight SMA with lookback and partial-window scaling
All checks were successful
CI / update (push) Successful in 3m54s
Fetch up to 6 extra measurements beyond the display limit so the SMA
window is fully populated from the first displayed point. For users
with fewer total measurements, use a reduced window with Bessel's
correction and sqrt(w/k) sigma scaling to reflect increased uncertainty.
2026-04-02 22:24:28 +02:00
eda87a8231 chore: remove dead migration and one-off scripts 2026-04-02 21:11:36 +02:00
5 changed files with 31 additions and 468 deletions

View File

@@ -1,69 +0,0 @@
# Formatter Replacement Progress
## Components Completed ✅
1. DebtBreakdown.svelte - Replaced formatCurrency function
2. EnhancedBalance.svelte - Replaced formatCurrency function (with Math.abs wrapper)
## Remaining Files to Update
### Components (3 files)
- [ ] PaymentModal.svelte - Has formatCurrency function
- [ ] SplitMethodSelector.svelte - Has inline .toFixed() calls
- [ ] BarChart.svelte - Has inline .toFixed() calls
- [ ] IngredientsPage.svelte - Has .toFixed() for recipe calculations
### Cospend Pages (7 files)
- [ ] routes/cospend/+page.svelte - Has formatCurrency function
- [ ] routes/cospend/payments/+page.svelte - Has formatCurrency function
- [ ] routes/cospend/payments/view/[id]/+page.svelte - Has formatCurrency and .toFixed()
- [ ] routes/cospend/payments/add/+page.svelte - Has .toFixed() and .toLocaleString()
- [ ] routes/cospend/payments/edit/[id]/+page.svelte - Has multiple .toFixed() calls
- [ ] routes/cospend/recurring/+page.svelte - Has formatCurrency function
- [ ] routes/cospend/recurring/edit/[id]/+page.svelte - Has .toFixed() and .toLocaleString()
- [ ] routes/cospend/settle/+page.svelte - Has formatCurrency function
## Replacement Strategy
### Pattern 1: Identical formatCurrency functions
```typescript
// OLD
function formatCurrency(amount) {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: 'CHF'
}).format(amount);
}
// NEW
import { formatCurrency } from '$lib/utils/formatters';
// Usage: formatCurrency(amount, 'CHF', 'de-CH')
```
### Pattern 2: .toFixed() for currency display
```typescript
// OLD
{payment.amount.toFixed(2)}
// NEW
import { formatNumber } from '$lib/utils/formatters';
{formatNumber(payment.amount, 2, 'de-CH')}
```
### Pattern 3: .toLocaleString() for dates
```typescript
// OLD
nextDate.toLocaleString('de-CH', { weekday: 'long', ... })
// NEW
import { formatDateTime } from '$lib/utils/formatters';
formatDateTime(nextDate, 'de-CH', { weekday: 'long', ... })
```
### Pattern 4: Exchange rate display (4 decimals)
```typescript
// OLD
{exchangeRate.toFixed(4)}
// NEW
{formatNumber(exchangeRate, 4, 'de-CH')}
```

View File

@@ -1,96 +0,0 @@
#!/usr/bin/env python3
"""
Script to replace inline formatCurrency functions with shared formatter utilities
"""
import re
import sys
files_to_update = [
"/home/alex/.local/src/homepage/src/routes/cospend/+page.svelte",
"/home/alex/.local/src/homepage/src/routes/cospend/payments/+page.svelte",
"/home/alex/.local/src/homepage/src/routes/cospend/payments/view/[id]/+page.svelte",
"/home/alex/.local/src/homepage/src/routes/cospend/recurring/+page.svelte",
"/home/alex/.local/src/homepage/src/routes/cospend/settle/+page.svelte",
]
def process_file(filepath):
print(f"Processing: {filepath}")
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
original_content = content
# Check if already has the import
has_formatter_import = 'from \'$lib/utils/formatters\'' in content or 'from "$lib/utils/formatters"' in content
# Find the <script> tag
script_match = re.search(r'(<script[^>]*>)', content)
if not script_match:
print(f" ⚠️ No <script> tag found")
return False
# Add import if not present
if not has_formatter_import:
script_tag = script_match.group(1)
# Find where to insert (after <script> tag)
script_end = script_match.end()
# Get existing imports to find the right place
imports_section_match = re.search(r'<script[^>]*>(.*?)(?:\n\n|\n export|\n let)', content, re.DOTALL)
if imports_section_match:
imports_end = imports_section_match.end() - len(imports_section_match.group(0).split('\n')[-1])
insert_pos = imports_end
else:
insert_pos = script_end
new_import = "\n import { formatCurrency } from '$lib/utils/formatters';"
content = content[:insert_pos] + new_import + content[insert_pos:]
print(f" ✓ Added import")
# Remove the formatCurrency function definition
# Pattern for the function with different variations
patterns = [
r'\n function formatCurrency\(amount\) \{\n return new Intl\.NumberFormat\(\'de-CH\',\s*\{\n\s*style:\s*\'currency\',\n\s*currency:\s*\'CHF\'\n\s*\}\)\.format\(amount\);\n \}',
r'\n function formatCurrency\(amount,\s*currency\s*=\s*\'CHF\'\) \{\n return new Intl\.NumberFormat\(\'de-CH\',\s*\{\n\s*style:\s*\'currency\',\n\s*currency:\s*currency\n\s*\}\)\.format\(amount\);\n \}',
]
for pattern in patterns:
if re.search(pattern, content):
content = re.sub(pattern, '', content)
print(f" ✓ Removed formatCurrency function")
break
# Check if content changed
if content != original_content:
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f" ✅ Updated successfully")
return True
else:
print(f" ⚠️ No changes needed")
return False
except Exception as e:
print(f" ❌ Error: {e}")
return False
def main():
print("=" * 60)
print("Replacing formatCurrency functions with shared utilities")
print("=" * 60)
success_count = 0
for filepath in files_to_update:
if process_file(filepath):
success_count += 1
print()
print("=" * 60)
print(f"Summary: {success_count}/{len(files_to_update)} files updated")
print("=" * 60)
if __name__ == "__main__":
main()

View File

@@ -1,205 +0,0 @@
import { dbConnect } from '../src/utils/db';
import { Exercise } from '../src/models/Exercise';
// ExerciseDB API configuration
const RAPIDAPI_KEY = process.env.RAPIDAPI_KEY || 'your-rapidapi-key-here';
const RAPIDAPI_HOST = 'exercisedb.p.rapidapi.com';
const BASE_URL = 'https://exercisedb.p.rapidapi.com';
interface ExerciseDBExercise {
id: string;
name: string;
gifUrl: string;
bodyPart: string;
equipment: string;
target: string;
secondaryMuscles: string[];
instructions: string[];
}
async function fetchFromExerciseDB(endpoint: string): Promise<any> {
const response = await fetch(`${BASE_URL}${endpoint}`, {
headers: {
'X-RapidAPI-Key': RAPIDAPI_KEY,
'X-RapidAPI-Host': RAPIDAPI_HOST
}
});
if (!response.ok) {
throw new Error(`Failed to fetch from ExerciseDB: ${response.status} ${response.statusText}`);
}
return response.json();
}
async function scrapeAllExercises(): Promise<void> {
console.log('🚀 Starting ExerciseDB scraping...');
try {
await dbConnect();
console.log('✅ Connected to database');
// Fetch all exercises
console.log('📡 Fetching exercises from ExerciseDB...');
const exercises: ExerciseDBExercise[] = await fetchFromExerciseDB('/exercises?limit=2000');
console.log(`📊 Found ${exercises.length} exercises`);
let imported = 0;
let skipped = 0;
let errors = 0;
for (const exercise of exercises) {
try {
// Check if exercise already exists
const existingExercise = await Exercise.findOne({ exerciseId: exercise.id });
if (existingExercise) {
skipped++;
continue;
}
// Determine difficulty based on equipment and complexity
let difficulty: 'beginner' | 'intermediate' | 'advanced' = 'intermediate';
if (exercise.equipment === 'body weight') {
difficulty = 'beginner';
} else if (exercise.equipment.includes('barbell') || exercise.equipment.includes('olympic')) {
difficulty = 'advanced';
} else if (exercise.equipment.includes('dumbbell') || exercise.equipment.includes('cable')) {
difficulty = 'intermediate';
}
// Create new exercise
const newExercise = new Exercise({
exerciseId: exercise.id,
name: exercise.name,
gifUrl: exercise.gifUrl,
bodyPart: exercise.bodyPart.toLowerCase(),
equipment: exercise.equipment.toLowerCase(),
target: exercise.target.toLowerCase(),
secondaryMuscles: exercise.secondaryMuscles.map(m => m.toLowerCase()),
instructions: exercise.instructions,
difficulty,
isActive: true
});
await newExercise.save();
imported++;
if (imported % 100 === 0) {
console.log(`⏳ Imported ${imported} exercises...`);
}
} catch (error) {
console.error(`❌ Error importing exercise ${exercise.name}:`, error);
errors++;
}
}
console.log('✅ Scraping completed!');
console.log(`📈 Summary: ${imported} imported, ${skipped} skipped, ${errors} errors`);
} catch (error) {
console.error('💥 Scraping failed:', error);
throw error;
}
}
async function updateExistingExercises(): Promise<void> {
console.log('🔄 Updating existing exercises...');
try {
await dbConnect();
const exercises: ExerciseDBExercise[] = await fetchFromExerciseDB('/exercises?limit=2000');
let updated = 0;
for (const exercise of exercises) {
try {
const existingExercise = await Exercise.findOne({ exerciseId: exercise.id });
if (existingExercise) {
// Update with new data from API
existingExercise.name = exercise.name;
existingExercise.gifUrl = exercise.gifUrl;
existingExercise.bodyPart = exercise.bodyPart.toLowerCase();
existingExercise.equipment = exercise.equipment.toLowerCase();
existingExercise.target = exercise.target.toLowerCase();
existingExercise.secondaryMuscles = exercise.secondaryMuscles.map(m => m.toLowerCase());
existingExercise.instructions = exercise.instructions;
await existingExercise.save();
updated++;
if (updated % 100 === 0) {
console.log(`⏳ Updated ${updated} exercises...`);
}
}
} catch (error) {
console.error(`❌ Error updating exercise ${exercise.name}:`, error);
}
}
console.log(`✅ Updated ${updated} exercises`);
} catch (error) {
console.error('💥 Update failed:', error);
throw error;
}
}
async function getExerciseStats(): Promise<void> {
try {
await dbConnect();
const totalExercises = await Exercise.countDocuments();
const activeExercises = await Exercise.countDocuments({ isActive: true });
const bodyParts = await Exercise.distinct('bodyPart');
const equipment = await Exercise.distinct('equipment');
const targets = await Exercise.distinct('target');
console.log('📊 Exercise Database Stats:');
console.log(` Total exercises: ${totalExercises}`);
console.log(` Active exercises: ${activeExercises}`);
console.log(` Body parts: ${bodyParts.length} (${bodyParts.join(', ')})`);
console.log(` Equipment types: ${equipment.length}`);
console.log(` Target muscles: ${targets.length}`);
} catch (error) {
console.error('💥 Stats failed:', error);
}
}
// CLI interface
const command = process.argv[2];
switch (command) {
case 'scrape':
scrapeAllExercises()
.then(() => process.exit(0))
.catch(() => process.exit(1));
break;
case 'update':
updateExistingExercises()
.then(() => process.exit(0))
.catch(() => process.exit(1));
break;
case 'stats':
getExerciseStats()
.then(() => process.exit(0))
.catch(() => process.exit(1));
break;
default:
console.log('Usage: tsx scripts/scrape-exercises.ts [command]');
console.log('Commands:');
console.log(' scrape - Import all exercises from ExerciseDB');
console.log(' update - Update existing exercises with latest data');
console.log(' stats - Show database statistics');
process.exit(0);
}

View File

@@ -1,73 +0,0 @@
#!/usr/bin/env python3
"""
Script to update formatCurrency calls to include CHF and de-CH parameters
"""
import re
files_to_update = [
"/home/alex/.local/src/homepage/src/routes/cospend/+page.svelte",
"/home/alex/.local/src/homepage/src/routes/cospend/payments/+page.svelte",
"/home/alex/.local/src/homepage/src/routes/cospend/payments/view/[id]/+page.svelte",
"/home/alex/.local/src/homepage/src/routes/cospend/recurring/+page.svelte",
"/home/alex/.local/src/homepage/src/routes/cospend/settle/+page.svelte",
]
def process_file(filepath):
print(f"Processing: {filepath}")
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
original_content = content
changes = 0
# Pattern 1: formatCurrency(amount) -> formatCurrency(amount, 'CHF', 'de-CH')
# But skip if already has parameters
def replace_single_param(match):
amount = match.group(1)
# Check if amount already contains currency parameter (contains comma followed by quote)
if ", '" in amount or ', "' in amount:
return match.group(0) # Already has parameters, skip
return f"formatCurrency({amount}, 'CHF', 'de-CH')"
content, count1 = re.subn(
r'formatCurrency\(([^)]+)\)',
replace_single_param,
content
)
changes += count1
if changes > 0:
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f" ✅ Updated {changes} formatCurrency calls")
return True
else:
print(f" ⚠️ No changes needed")
return False
except Exception as e:
print(f" ❌ Error: {e}")
import traceback
traceback.print_exc()
return False
def main():
print("=" * 60)
print("Updating formatCurrency calls with CHF and de-CH params")
print("=" * 60)
success_count = 0
for filepath in files_to_update:
if process_file(filepath):
success_count += 1
print()
print("=" * 60)
print(f"Summary: {success_count}/{len(files_to_update)} files updated")
print("=" * 60)
if __name__ == "__main__":
main()

View File

@@ -120,13 +120,20 @@ export const GET: RequestHandler = async ({ locals }) => {
upper: totalKcal + combinedMargin,
};
// Fetch extra measurements beyond the display limit to fill the SMA lookback window
const DISPLAY_LIMIT = 30;
const SMA_LOOKBACK = 6; // w - 1 where w = 7 max
const weightMeasurements = await BodyMeasurement.find(
{ createdBy: user.nickname, weight: { $ne: null } },
{ date: 1, weight: 1, _id: 0 }
)
.sort({ date: 1 })
.limit(30)
.sort({ date: -1 })
.limit(DISPLAY_LIMIT + SMA_LOOKBACK)
.lean();
weightMeasurements.reverse(); // back to chronological order
// Split into lookback-only (not displayed) and display portions
const displayStart = Math.max(0, weightMeasurements.length - DISPLAY_LIMIT);
// Build chart-ready workouts-per-week with filled gaps
const weekMap = new Map<string, number>();
@@ -161,39 +168,38 @@ export const GET: RequestHandler = async ({ locals }) => {
upper: (number | null)[];
lower: (number | null)[];
} = { labels: [], dates: [], data: [], sma: [], upper: [], lower: [] };
const weights: number[] = [];
for (const m of weightMeasurements) {
const d = new Date(m.date);
const allWeights: number[] = weightMeasurements.map(m => m.weight!);
for (let idx = displayStart; idx < weightMeasurements.length; idx++) {
const d = new Date(weightMeasurements[idx].date);
weightChart.labels.push(
d.toLocaleDateString('en', { month: 'short', day: 'numeric' })
);
weightChart.dates.push(d.toISOString());
weightChart.data.push(m.weight!);
weights.push(m.weight!);
weightChart.data.push(allWeights[idx]);
}
// Adaptive window: 7 if enough data, otherwise half the data (min 2)
const w = Math.min(7, Math.max(2, Math.floor(weights.length / 2)));
for (let i = 0; i < weights.length; i++) {
if (i < w - 1) {
weightChart.sma.push(null);
weightChart.upper.push(null);
weightChart.lower.push(null);
} else {
const w = Math.min(7, Math.max(2, Math.floor(allWeights.length / 2)));
for (let idx = displayStart; idx < allWeights.length; idx++) {
// Use full window when available, otherwise use all points so far
const k = Math.min(w, idx + 1);
let sum = 0;
for (let j = i - w + 1; j <= i; j++) sum += weights[j];
const mean = sum / w;
for (let j = idx - k + 1; j <= idx; j++) sum += allWeights[j];
const mean = sum / k;
let variance = 0;
for (let j = i - w + 1; j <= i; j++) variance += (weights[j] - mean) ** 2;
const std = Math.sqrt(variance / w);
for (let j = idx - k + 1; j <= idx; j++) variance += (allWeights[j] - mean) ** 2;
// Bessel's correction (k-1) for unbiased sample variance;
// scale by sqrt(w/k) so the band widens when k < w
const std = k > 1
? Math.sqrt(variance / (k - 1)) * Math.sqrt(w / k)
: Math.sqrt(variance) * Math.sqrt(w);
const round = (v: number) => Math.round(v * 100) / 100;
weightChart.sma.push(round(mean));
weightChart.upper.push(round(mean + std));
weightChart.lower.push(round(mean - std));
}
}
return json({
totalWorkouts,