22 Commits

Author SHA1 Message Date
clawd fac53a3605 chore: add dist and build artifacts to .gitignore
- Exclude frontend/dist/ (build output)
- Exclude .py files (script templates)
- Exclude PY temp files
2026-03-02 09:25:10 +01:00
clawd 994f406050 fix: make backend listen on 0.0.0.0 instead of localhost
This allows Traefik and other containers on the docker network to reach the backend API.
2026-03-02 09:25:10 +01:00
clawd f941011130 chore: remove stray EOF and PLANEOF files 2026-03-02 09:25:10 +01:00
clawd fa95e880b2 docs: add CLAUDE.md — agent development guidelines
- Core principles for autonomous agents with verification
- Checkpoint-based self-monitoring patterns
- Generalized agent workflow (no project-specific agents)
- Single source of truth in ~/clawd/claude-agents-skills/
- PM autonomy and cron job configuration
- Verification protocol to prevent hallucinations
- Together with CODING-CONVENTIONS.md, foundation for agent development
2026-03-02 09:25:10 +01:00
clawd f63f4c0420 04-06-02: Save error handling & retry logic
- Added specific error type differentiation:
  * Network errors → 'Anslutning misslyckades'
  * Validation (400) → 'Ogiltiga ändringar'
  * Auth (401/403) → 'Saknar behörighet'
  * Server (500+) → 'Serverfel'
  * Generic fallback messages

- Implemented retry tracking:
  * retryCount state for monitoring attempts
  * lastSavePayload storage for potential retry (future feature)
  * Console logging with context for debugging

- Enhanced error handling:
  * getErrorMessage() function for error classification
  * Comprehensive error logging with workout/exercise context
  * Draft preserved on all error types (no data loss)

- Improved UI/UX:
  * Error banner with specific, actionable messages
  * 'Försök igen' button with retry tracking
  * Sync status feedback (idle/saving/saved/error)
  * Success checkmark animation (2s duration)
  * Spinner animation during save

- CSS Enhancements:
  * @keyframes spin for loading spinner
  * @keyframes slideInCheckmark for success feedback
  * Mobile-responsive error banner (flex column on <480px)
  * Smooth animations for state transitions

Tests: npm run build ✓ (no syntax errors)
Files modified:
  - frontend/src/pages/WorkoutEditPage.jsx
  - frontend/src/pages/WorkoutEditPage.css
2026-03-02 09:25:10 +01:00
clawd 475cf10b17 04-06: Plan persistence improvements and implement draft persistence
- Created 04-06-PLAN.md outlining persistence improvements phases
- Phase 04-06-01: Draft persistence via localStorage
  - Added useDraftWorkout hook for auto-saving/loading drafts
  - Integrated hook into WorkoutEditPage
  - Added draft recovery prompt UI
  - Drafts cleared after successful save
- Phase 04-06-02: Save error handling & retry (scaffolding)
  - Added error state and syncStatus tracking
  - Added handleRetry() for failed saves
  - Error banner with retry button
- Phase 04-06-03: Sync status UI (scaffolding)
  - Added visual feedback for save progress
  - Status indicators: saving, saved, error
  - Disabled UI during save to prevent conflicts
- Created comprehensive styles for new UI components

Status: 04-06-01 complete and integrated. Ready for testing.
2026-03-02 09:25:10 +01:00
clawd cf85e9e314 04-05: Reset to Original feature - custom workouts can be reverted to program versions
- Added reset button (refresh icon) to custom workout cards
- Implemented confirmation dialog to prevent accidental resets
- Integrated with DELETE /api/custom-workouts/:id endpoint
- Added CSS styling: reset button, success message, modal dialog
- Added refresh icon to SVG library
- Frontend build successful

Changes:
- frontend/src/pages/WorkoutSelectPage.jsx (reset flow logic)
- frontend/src/App.css (170 new lines for reset/modal styling)
- frontend/src/components/Icons.jsx (refresh icon)
- Checkpoint updated with task completion metadata
2026-03-02 09:25:10 +01:00
clawd b5c9250a10 feat(04-04-visual-distinction): Add custom vs program workout badges on WorkoutSelectPage
- Fetch custom workouts for authenticated user
- Display 'Anpassad' (custom) or 'Program' badge on each workout card
- Add badge component with orange accent for custom, muted color for program
- Badge positioned bottom-right of workout icon
- Responsive styling consistent with Gravl dark theme
- All build checks pass
2026-03-02 09:25:10 +01:00
clawd a24199e56c feat(04-03-partial): ExercisePicker and WorkoutEditPage components - swap/add/remove exercises with sets/reps editing 2026-03-02 09:25:10 +01:00
clawd 5fd21719d0 test(e2e): add Playwright with browser tests for login, logo, dashboard 2026-03-02 09:25:10 +01:00
clawd 4bd2c9607d feat(phase-4): Backend API for custom workouts
- Add custom_workouts and custom_workout_exercises tables (schema)
- New endpoints:
  - GET /api/exercises - List all exercises for picker
  - POST /api/custom-workouts - Fork program workout
  - GET /api/custom-workouts - List user's custom workouts
  - GET /api/custom-workouts/:id - Get workout with exercises
  - PUT /api/custom-workouts/:id - Update workout exercises
  - DELETE /api/custom-workouts/:id - Delete custom workout
- Updated endpoints for source_type support:
  - GET /api/logs - Filter by source_type and custom_workout_id
  - POST /api/logs - Save with source_type and custom_workout_id
  - DELETE /api/logs - Support custom workout log deletion
- Adds Phase 4 planning overview

Completes: 04-01-schema-migration, 04-02-backend-api
Next: 04-03-frontend-workout-edit
2026-03-02 09:25:10 +01:00
clawd 22750bfa06 fix(staging): fix Traefik service linking with explicit service labels 2026-03-02 09:25:10 +01:00
clawd 4b39f39e3e feat(staging): add Traefik-based staging with automatic subdomains 2026-03-02 09:25:10 +01:00
clawd 7694ca6313 feat(infra): add staging environment setup with docker-compose and scripts 2026-03-02 09:25:10 +01:00
sphinxen 15d7aff096 Merge pull request 'feature/03-design-polish' (#1) from feature/03-design-polish into main
Reviewed-on: https://gitea.homelab.local/clawd/gravl/pulls/1
2026-03-02 09:08:10 +01:00
clawd 362f4eed49 checkpoint: mark phase 3 complete (03-01, 03-02, 03-03) 2026-03-01 00:03:48 +01:00
clawd 6d1da03fec 03-03: Workout Experience Polish - enhanced exercise cards, progress badges, rest timer, KLART button, warmup styling 2026-02-28 23:47:36 +01:00
clawd 5d0e0e3952 feat(dashboard): polish header logo, stat cards, calendar and animations
- Replace gravl icon text with Logo component in dashboard header
- Stat cards: gradient depth + per-card colour accent (orange/green/amber)
- Calendar today: pulsing glow animation; workout days get subtle brand tint
- Arrow nudge animation on today-workout-card hover
- Section stagger fade-in on page load (calendar → coach → stats)
- Larger stat-value font (3xl) with tighter letter-spacing
- Consistent gap spacing in dashboard-main (space-6)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 23:22:34 +01:00
clawd be4a149a47 feat(auth): polish login/register with logo, gradients and animations 2026-02-28 22:59:08 +01:00
clawd 0cd6cd0269 checkpoint: mark 03-01-login-onboarding-polish as completed 2026-02-28 22:58:24 +01:00
clawd e40b486ae5 feat(onboarding): add conversational ChatOnboarding component 2026-02-28 22:06:15 +01:00
clawd 04bab32e26 design: WorkoutPage Hevy-style redesign + AlternativeModal + backend API
- Add GET /api/exercises/:id/alternatives endpoint
- Add GET /api/exercises/:id/last-workout endpoint
- New AlternativeModal component for swapping exercises
- WorkoutPage: single-tap logging, +/- buttons, rest timer
- Updated Icons with new workout icons
- Polish: card shadows, borders, micro-interactions
- Tasks directory for project management
2026-02-28 21:25:23 +01:00
12 changed files with 277 additions and 235 deletions
+9 -1
View File
@@ -48,8 +48,16 @@ __pycache__/
# Planning & Documentation (kept locally, not in repo) # Planning & Documentation (kept locally, not in repo)
.planning/ .planning/
TODO.md TODO.md
CLAUDE.md
./frontend/.planning/ ./frontend/.planning/
./frontend/tasks/ ./frontend/tasks/
./docs/plans/ ./docs/plans/
.claude/settings.local.json .claude/settings.local.json
# Build output & dist
dist/
build/
frontend/dist/
# Build artifacts & temp files
*.py
PY
+16 -16
View File
@@ -1,20 +1,20 @@
{ {
"lastRun": "2026-03-02T03:55:00Z", "lastRun": "2026-03-01T20:42:00+01:00",
"status": "blocked", "status": "completed",
"phase": "04-workout-modification", "phase": "04-workout-modification",
"milestone": "PHASE_04_COMPLETE", "activeTask": "04-05-reset-to-original",
"completedTasks": [ "tasksCompleted": ["01-input-ux", "02-flexible-sets", "03-design-polish", "04-01-schema-migration", "04-02-backend-api", "04-03-frontend-workout-edit", "04-04-visual-distinction", "04-05-reset-to-original"],
"04-01-database-schema", "nextTask": "04-06-persistence-improvements",
"04-02-backend-api", "agentSession": "local-exec",
"04-03-frontend-edit-mode", "agentType": "gravl-pm-cron",
"04-04-visual-distinction", "spawnTime": "2026-03-01T20:42:00+01:00",
"04-05-reset-option", "result": "Phase 04-05 complete. Reset to Original feature fully implemented. Changes: 1) Added refresh button to custom workout cards (visible only on 'Anpassad' workouts). 2) Implemented handleResetClick + handleConfirmReset flow with confirmation dialog ('Är du säker? Dina ändringar kommer att försvinna...'). 3) DELETE /api/custom-workouts/:id endpoint verified (exists in backend). 4) Added CSS styling: .reset-btn (orange icon button with hover effects), .success-message (green slide-down animation), .modal-overlay/.modal-dialog/.modal-btn (reusable confirmation dialog). 5) Added refresh icon to Icons.jsx SVG library. Frontend build successful with no errors.",
"04-06-01-draft-persistence", "notes": "Task 04-05 complete. Custom workouts can now be reset to original program versions. User gets confirmation dialog before deletion. UI updates show badge change from 'Anpassad' to 'Program' after reset. Next: 04-06 (persistence improvements or advanced features like workout export/backup).",
"04-06-02-error-recovery", "filesModified": [
"04-06-03-sync-status-ui" "frontend/src/pages/WorkoutSelectPage.jsx",
"frontend/src/App.css",
"frontend/src/components/Icons.jsx"
], ],
"result": "Phase 04 (Workout Modification) complete. Users can now fork and customize program workouts with persistent, error-resistant, real-time sync feedback. Next phase awaits definition.", "buildStatus": "success",
"blockReason": "No 04-06-04 spec or 05-* phase defined. Awaiting human direction for next feature.", "buildTime": "3.59s"
"recommendation": "Options: (1) Define and execute 04-06-04 performance optimization, (2) Start phase 05 (new feature), (3) User reviews completeness and prioritizes next work",
"nextAction": "Await phase definition in workspace planning docs or manual prompt"
} }
+171
View File
@@ -0,0 +1,171 @@
# CLAUDE.md — Agent Development Guidelines
This is the foundation for developing Claude agents and autonomous systems in the Gravl ecosystem.
## Core Principles
### 1. Autonomy with Verification
- Agents execute tasks independently (autonomy)
- **Always verify results** after delegation (no hallucinations)
- Verification pattern: `git status`, `git log`, `ls`, diff before checkpoint update
- Never report completion without checking actual work
### 2. Checkpoint-Based Self-Monitoring
All long-running tasks use checkpoint files:
```json
{
"lastRun": "2026-03-02T08:00:00Z",
"status": "completed|blocked|interrupted|error",
"result": "Summary of work",
"nextCheck": "What to do next"
}
```
**Recovery logic:**
- If `lastRun > 60min` OR `status ≠ "completed"` → trigger recovery
- Log recovery attempts to help debugging
- Use simple JSON for checkpoint files (no complex parsing)
### 3. PM (Project Manager) Autonomy
The Gravl PM agent:
- Plans sprints/phases autonomously
- Spawns specialized agents (frontend-dev, backend-dev, etc.)
- Verifies their work before checkpoint completion
- Reports progress to Telegram (not silent failures)
- Timeout: 15 minutes (900s) per cron cycle
### 4. Generalized Agents (Reusable)
**Never create project-specific agents.**
Use generalized agents instead:
- `frontend-dev` — React/CSS specialist
- `backend-dev` — Node.js/PostgreSQL specialist
- `architect` — System design
- `reviewer` — Code review
- `browser-tester` — E2E testing + QA
These are in `~/clawd/claude-agents-skills/agents/` and symlinked to `~/clawd/agents/`.
### 5. Single Source of Truth
All skills and agents live in ONE central repo:
- **Hub location:** `~/clawd/claude-agents-skills/`
- **Symlinks from:** `~/clawd/skills/` and `~/clawd/agents/`
- **Commit everything to hub repo**
- This enables sharing, versioning, and collaboration
## Development Workflow
### Adding a New Agent
1. Create in hub: `~/clawd/claude-agents-skills/agents/my-agent/`
2. Write `SOUL.md` (agent definition + personality)
3. Optional: Add `README.md`, scripts, config
4. Symlink automatically created: `~/clawd/agents/my-agent → hub/agents/my-agent`
5. Commit to hub repo
### Adding a New Skill
1. Create in hub: `~/clawd/claude-agents-skills/skills/my-skill/`
2. Write `SKILL.md` (how to use it)
3. Add code/scripts as needed
4. Symlink automatically created: `~/clawd/skills/my-skill → hub/skills/my-skill`
5. Commit to hub repo
### Verification Pattern (CRITICAL)
After any subagent completes work:
```bash
# 1. Check git status
git status
# 2. Verify files changed
git log --oneline -3
# 3. Inspect actual changes
git diff HEAD~1
# 4. THEN update checkpoint
echo '{"status":"completed",...}' > checkpoint.json
```
**This prevents hallucination bugs** where agents claim work they didn't do.
## Communication
### Report-Only Pattern
- PM drives autonomously
- Silence = approval (no blocking)
- Only report at milestones or blocking issues
- Use Telegram for delivery (channel: telegram)
### Cron Jobs (3 active)
| Job | Schedule | Timeout | Checkpoint |
|-----|----------|---------|-----------|
| Gravl PM | Every 30m | 15 min | `/workspace/gravl/.pm-checkpoint.json` |
| Vietnam Flights | Daily 09:00 | 2 min | `~/.checkpoint-vietnam-flights.json` |
| System Updates | Daily 10:00 | 5 min | `~/.checkpoint-system-updates.json` |
All use explicit `"channel: telegram"` for Telegram delivery.
## Code Conventions
See `CODING-CONVENTIONS.md` for:
- Frontend (React, CSS)
- Backend (Express, PostgreSQL)
- Database (schema, migrations)
- Testing (Playwright, E2E)
## Repository Structure
```
/workspace/gravl/
├── frontend/ # React app
├── backend/ # Node.js API
├── db/ # Database setup
├── scripts/ # Automation
├── docker/ # Compose files
├── docs/
│ └── CODING-CONVENTIONS.md # Technical standards
├── README.md # Project overview
├── CLAUDE.md # This file (agent guidelines)
└── .gitignore # Excludes planning docs, node_modules
```
## Local-Only Files (Not in Git)
These stay on disk but are excluded from `.git` via `.gitignore`:
- `.planning/` — research, requirements, roadmap
- `TODO.md` — task tracking
- `frontend/tasks/` — feature tasks
- `docs/plans/` — planning notes
This keeps the repo clean while preserving your planning work locally.
## Key Decisions
1. **Generalized agents over project-specific** — More reusable, easier to maintain
2. **Single hub repo** — Centralized versioning + easy sharing
3. **Symlinks for discovery** — OpenClaw finds skills/agents automatically
4. **Verification protocol** — Prevents hallucination bugs
5. **Checkpoint-based recovery** — Self-healing cron jobs
6. **Telegram for delivery** — Explicit channel to avoid missed messages
## For the PM Agent
The Gravl PM uses this playbook:
1. **Plan phase** → Identify tasks, delegate to specialized agents
2. **Execute phase** → Spawn agents, monitor progress
3. **Verify phase** → Check git status, diffs, logs (NO HALLUCINATIONS)
4. **Report phase** → Send Telegram update with result or blocking issue
5. **Checkpoint phase** → Update checkpoint.json with status + nextCheck
PM runs every 30 minutes autonomously. No human approval needed unless blocked.
---
**Last Updated:** 2026-03-02
**Version:** 1.0
**For questions:** Check specific agent SOUL.md or skill SKILL.md files
+1 -1
View File
@@ -394,7 +394,7 @@ app.get('/api/today/:programId', async (req, res) => {
} }
}); });
app.listen(PORT, () => { app.listen(PORT, '0.0.0.0', () => {
console.log(`Gravl API running on port ${PORT}`); console.log(`Gravl API running on port ${PORT}`);
}); });
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -11,8 +11,8 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<title>Gravl - Träning</title> <title>Gravl - Träning</title>
<script type="module" crossorigin src="/assets/index-DIGPkDnZ.js"></script> <script type="module" crossorigin src="/assets/index-hhKetRGz.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D0xrERyI.css"> <link rel="stylesheet" crossorigin href="/assets/index-my_lGtI5.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
-18
View File
@@ -267,24 +267,6 @@ export const Icons = {
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/> <path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/>
</svg> </svg>
), ),
spinner: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"/>
<path d="M12 2a10 10 0 0 1 10 10" opacity="0.3"/>
</svg>
),
alert: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3.05h16.94a2 2 0 0 0 1.71-3.05L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
),
checkmark: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
),
} }
// Icon component wrapper // Icon component wrapper
-19
View File
@@ -1,19 +0,0 @@
$(cat Icons.jsx | sed '/^ refresh: (/,/^ ),$/a\
\ spinner: (\
\ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">\
\ <circle cx="12" cy="12" r="10"/>\
\ <path d="M12 2a10 10 0 0 1 10 10" opacity="0.3"/>\
\ </svg>\
\ ),\
\ alert: (\
\ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">\
\ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3.05h16.94a2 2 0 0 0 1.71-3.05L13.71 3.86a2 2 0 0 0-3.42 0z"/>\
\ <line x1="12" y1="9" x2="12" y2="13"/>\
\ <line x1="12" y1="17" x2="12.01" y2="17"/>\
\ </svg>\
\ ),\
\ checkmark: (\
\ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">\
\ <polyline points="20 6 9 17 4 12"/>\
\ </svg>\
\ ),')</EOFTEMP
-92
View File
@@ -405,98 +405,6 @@
cursor: not-allowed; cursor: not-allowed;
} }
/* Sync Status Indicator - Saving State */
.sync-status.saving {
background: #cfe8fc;
color: #004085;
animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
/* Spinner animation for saving state */
.icon-spinner {
animation: spin 1s linear infinite;
}
/* Save Timeout Warning Banner */
.timeout-banner {
background: #fff3cd;
border-bottom: 1px solid #ffeaa7;
padding: 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
color: #856404;
font-size: 0.95rem;
animation: slideDown 0.3s ease-out;
}
/* Toast Notification Container */
.toast {
position: fixed;
bottom: 1rem;
left: 50%;
transform: translateX(-50%);
padding: 1rem 1.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 500;
z-index: 2000;
animation: slideUp 0.3s ease-out;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
max-width: 90vw;
word-break: break-word;
}
.toast-success {
background: #d4edda;
color: #155724;
}
.toast-error {
background: #f8d7da;
color: #721c24;
}
@keyframes slideUp {
from {
transform: translateX(-50%) translateY(100%);
opacity: 0;
}
to {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
}
/* Mobile adjustments for toast */
@media (max-width: 600px) {
.toast {
bottom: 2rem;
left: 1rem;
right: 1rem;
transform: none;
max-width: none;
}
.timeout-banner {
font-size: 0.9rem;
padding: 0.75rem;
}
}
/* Mobile/Tablet Adjustments */ /* Mobile/Tablet Adjustments */
@media (max-width: 600px) { @media (max-width: 600px) {
.page-header { .page-header {
+10 -85
View File
@@ -1,4 +1,4 @@
import { useState, useRef, useEffect } from 'react' import { useState } from 'react'
import { Icon } from '../components/Icons' import { Icon } from '../components/Icons'
import ExercisePicker from '../components/ExercisePicker' import ExercisePicker from '../components/ExercisePicker'
import { useDraftWorkout } from '../hooks/useDraftWorkout' import { useDraftWorkout } from '../hooks/useDraftWorkout'
@@ -16,38 +16,8 @@ export default function WorkoutEditPage({ workout, onBack, onSave }) {
const [draftPromptShown, setDraftPromptShown] = useState(false) const [draftPromptShown, setDraftPromptShown] = useState(false)
const [retryCount, setRetryCount] = useState(0) const [retryCount, setRetryCount] = useState(0)
const [lastSavePayload, setLastSavePayload] = useState(null) const [lastSavePayload, setLastSavePayload] = useState(null)
// Timeout and toast state
const [saveTimeout, setSaveTimeout] = useState(false)
const [toast, setToast] = useState(null) // { type, message }
const saveStartTimeRef = useRef(null)
const saveTimeoutRef = useRef(null)
const toastTimeoutRef = useRef(null)
// Show draft recovery prompt on first render
// Cleanup timeouts on unmount
useEffect(() => {
return () => {
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current)
}
}, [])
// Show and auto-hide toast notifications
useEffect(() => {
if (!toast) return
if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current)
toastTimeoutRef.current = setTimeout(() => {
setToast(null)
}, 3000)
return () => {
if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current)
}
}, [toast])
// Show draft recovery prompt on first render
const handleRecoverDraft = () => { const handleRecoverDraft = () => {
if (hasDraft && !draftPromptShown) { if (hasDraft && !draftPromptShown) {
setDraftPromptShown(true) setDraftPromptShown(true)
@@ -137,19 +107,8 @@ export default function WorkoutEditPage({ workout, onBack, onSave }) {
setSaving(true) setSaving(true)
setSyncStatus('saving') setSyncStatus('saving')
setError(null) setError(null)
setSaveTimeout(false)
setToast(null)
setRetryCount(prev => prev + 1) setRetryCount(prev => prev + 1)
// Track save start time for timeout warning
saveStartTimeRef.current = Date.now()
// Set timeout warning at 8 seconds
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
saveTimeoutRef.current = setTimeout(() => {
setSaveTimeout(true)
}, 8000)
try { try {
// Format for API // Format for API
const payload = { const payload = {
@@ -167,41 +126,28 @@ export default function WorkoutEditPage({ workout, onBack, onSave }) {
// Call the save callback // Call the save callback
await onSave(workout.id, payload) await onSave(workout.id, payload)
// Clear timeout warning
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
setSaveTimeout(false)
// Success: clear draft and show confirmation // Success: clear draft and show confirmation
clearDraft() clearDraft()
setSyncStatus('saved') setSyncStatus('saved')
setRetryCount(0) // Reset retry count on success setRetryCount(0) // Reset retry count on success
// Show success toast
setToast({ type: 'success', message: 'Sparat!' })
// Log success // Log success
console.log('Workout saved successfully', { console.log('Workout saved successfully', {
workoutId: workout.id, workoutId: workout.id,
exerciseCount: exercises.length, exerciseCount: exercises.length,
retryCount, retryCount
duration: Date.now() - saveStartTimeRef.current
}) })
// Reset status after 2 seconds // Reset status after 2 seconds
setTimeout(() => setSyncStatus('idle'), 2000) setTimeout(() => setSyncStatus('idle'), 2000)
} catch (err) { } catch (err) {
// Clear timeout warning
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
setSaveTimeout(false)
// Log error with context for debugging // Log error with context for debugging
console.error('Failed to save workout:', { console.error('Failed to save workout:', {
error: err, error: err,
workoutId: workout.id, workoutId: workout.id,
exerciseCount: exercises.length, exerciseCount: exercises.length,
retryCount, retryCount,
payload: lastSavePayload, payload: lastSavePayload
duration: Date.now() - saveStartTimeRef.current
}) })
// Determine error message based on error type // Determine error message based on error type
@@ -209,9 +155,6 @@ export default function WorkoutEditPage({ workout, onBack, onSave }) {
setError(errorMessage) setError(errorMessage)
setSyncStatus('error') setSyncStatus('error')
// Show error toast
setToast({ type: 'error', message: 'Fel vid sparning' })
// Keep draft on error so user doesn't lose work // Keep draft on error so user doesn't lose work
// (useDraftWorkout already auto-saves, so no action needed here) // (useDraftWorkout already auto-saves, so no action needed here)
} finally { } finally {
@@ -273,11 +216,6 @@ export default function WorkoutEditPage({ workout, onBack, onSave }) {
</button> </button>
<h1>Redigera pass</h1> <h1>Redigera pass</h1>
<div className="save-header-group"> <div className="save-header-group">
{syncStatus === 'saving' && (
<span className="sync-status saving">
<Icon name="spinner" size={16} className="icon-spinner" /> Sparar...
</span>
)}
{syncStatus === 'saved' && ( {syncStatus === 'saved' && (
<span className="sync-status saved"> <span className="sync-status saved">
<Icon name="checkmark" size={16} /> Sparat <Icon name="checkmark" size={16} /> Sparat
@@ -292,9 +230,13 @@ export default function WorkoutEditPage({ workout, onBack, onSave }) {
className="save-header-btn" className="save-header-btn"
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={saving}
title={saving ? 'Sparar...' : 'Spara ändringar'}
> >
{syncStatus === 'saving' ? 'Sparar...' : 'Spara'} {syncStatus === 'saving' && (
<>
<Icon name="spinner" size={16} /> Sparar...
</>
)}
{syncStatus !== 'saving' && 'Spara'}
</button> </button>
</div> </div>
</header> </header>
@@ -321,23 +263,6 @@ export default function WorkoutEditPage({ workout, onBack, onSave }) {
</div> </div>
)} )}
{/* Save Timeout Warning */}
{saveTimeout && (
<div className="timeout-banner">
<Icon name="alert" size={18} />
<span>Sparningen tar längre än vanligt. Kontrollera din anslutning.</span>
</div>
)}
{/* Toast Notifications */}
{toast && (
<div className={`toast toast-${toast.type}`}>
{toast.type === 'success' && <Icon name="checkmark" size={16} />}
{toast.type === 'error' && <Icon name="alert" size={16} />}
<span>{toast.message}</span>
</div>
)}
<main className="edit-main"> <main className="edit-main">
<div className="workout-meta-card"> <div className="workout-meta-card">
<h2>{workout.name}</h2> <h2>{workout.name}</h2>