Root cause: Template expects flat structure (metrics.database.count) but monitoring module provides nested structure (metrics.by_type.database.count) with different field names (avg_duration_ms vs avg). Solution: Route Adapter Pattern - transformer function maps data structure at presentation layer. Changes: - Add transform_metrics_for_template() function to admin.py - Update metrics_dashboard() route to use transformer - Provide safe defaults for missing/empty metrics data - Handle all operation types: database, http, render Testing: All 32 admin route tests passing Documentation: - Updated implementation report with actual fix details - Created consolidated hotfix design documentation - Architectural review by architect (approved with minor concerns) Technical debt: Adapter layer should be replaced with proper data contracts in v1.2.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
115 lines
3.9 KiB
Markdown
115 lines
3.9 KiB
Markdown
# Hotfix Design: v1.1.1-rc.2 - Metrics Dashboard Template Data Mismatch
|
|
|
|
## Problem Summary
|
|
|
|
Production deployment of v1.1.1-rc.1 exposed two critical issues in the metrics dashboard:
|
|
|
|
1. **Route Conflict** (Fixed in initial attempt): Two routes mapped to similar paths causing ambiguity
|
|
2. **Template/Data Mismatch** (Root cause): Template expects different data structure than monitoring module provides
|
|
|
|
### The Template/Data Mismatch
|
|
|
|
**Template Expects** (`metrics_dashboard.html` line 163):
|
|
```jinja2
|
|
{{ metrics.database.count|default(0) }}
|
|
{{ metrics.database.avg|default(0) }}
|
|
{{ metrics.database.min|default(0) }}
|
|
{{ metrics.database.max|default(0) }}
|
|
```
|
|
|
|
**Monitoring Module Returns**:
|
|
```python
|
|
{
|
|
"by_type": {
|
|
"database": {
|
|
"count": 50,
|
|
"avg_duration_ms": 12.5,
|
|
"min_duration_ms": 2.0,
|
|
"max_duration_ms": 45.0
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Note the two mismatches:
|
|
1. **Nesting**: Template wants `metrics.database` but gets `metrics.by_type.database`
|
|
2. **Field Names**: Template wants `avg` but gets `avg_duration_ms`
|
|
|
|
## Solution: Route Adapter Pattern
|
|
|
|
Transform data at the presentation layer (route handler) to match template expectations.
|
|
|
|
### Implementation
|
|
|
|
Added a transformer function in `admin.py` that:
|
|
1. Flattens the nested structure (`by_type.database` → `database`)
|
|
2. Maps field names (`avg_duration_ms` → `avg`)
|
|
3. Provides safe defaults for missing data
|
|
|
|
```python
|
|
def transform_metrics_for_template(metrics_stats):
|
|
"""Transform metrics stats to match template structure"""
|
|
transformed = {}
|
|
|
|
# Map by_type to direct access with field name mapping
|
|
for op_type in ['database', 'http', 'render']:
|
|
if 'by_type' in metrics_stats and op_type in metrics_stats['by_type']:
|
|
type_data = metrics_stats['by_type'][op_type]
|
|
transformed[op_type] = {
|
|
'count': type_data.get('count', 0),
|
|
'avg': type_data.get('avg_duration_ms', 0), # Note field name change
|
|
'min': type_data.get('min_duration_ms', 0),
|
|
'max': type_data.get('max_duration_ms', 0)
|
|
}
|
|
else:
|
|
# Safe defaults
|
|
transformed[op_type] = {'count': 0, 'avg': 0, 'min': 0, 'max': 0}
|
|
|
|
# Keep other top-level stats
|
|
transformed['total_count'] = metrics_stats.get('total_count', 0)
|
|
transformed['max_size'] = metrics_stats.get('max_size', 1000)
|
|
transformed['process_id'] = metrics_stats.get('process_id', 0)
|
|
|
|
return transformed
|
|
```
|
|
|
|
### Why This Approach?
|
|
|
|
1. **Minimal Risk**: Only changes route handler, not core monitoring module
|
|
2. **Preserves API**: Monitoring module remains unchanged for other consumers
|
|
3. **No Template Changes**: Avoids modifying template and JavaScript
|
|
4. **Clear Separation**: Route acts as adapter between business logic and view
|
|
|
|
## Additional Fixes Applied
|
|
|
|
1. **Route Path Change**: `/admin/dashboard` → `/admin/metrics-dashboard` (prevents conflict)
|
|
2. **Defensive Imports**: Graceful handling of missing monitoring module
|
|
3. **Error Handling**: Safe defaults when metrics collection fails
|
|
|
|
## Testing and Validation
|
|
|
|
Created comprehensive test script validating:
|
|
- Data structure transformation works correctly
|
|
- All template fields accessible after transformation
|
|
- Safe defaults provided for missing data
|
|
- Field name mapping correct
|
|
|
|
All 32 admin route tests pass with 100% success rate.
|
|
|
|
## Files Modified
|
|
|
|
1. `/starpunk/routes/admin.py`:
|
|
- Lines 218-260: Added transformer function
|
|
- Line 263: Changed route path
|
|
- Lines 285-314: Applied transformer and added error handling
|
|
|
|
2. `/starpunk/__init__.py`: Version bump to 1.1.1-rc.2
|
|
|
|
3. `/CHANGELOG.md`: Documented hotfix
|
|
|
|
## Production Impact
|
|
|
|
**Before**: 500 error with `'dict object' has no attribute 'database'`
|
|
**After**: Metrics dashboard loads correctly with properly structured data
|
|
|
|
This is a tactical bug fix, not an architectural change, and should be documented as such. |