Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 86f2baef44 | |||
| 34a0c3b60d | |||
| 219a52e0f2 | |||
| 120a629eea | |||
| d737d39f50 | |||
| cac31e85bb | |||
| cf009ad975 | |||
| a2a3949269 | |||
| 81e0caab42 | |||
| e923a17707 | |||
| 0ebcf54e09 | |||
| 5229d793a9 | |||
| b15497012c | |||
| b413d1755b | |||
| 4e73d17fb3 | |||
| 12fca716ac | |||
| f93bea69c6 | |||
| aad04e3387 | |||
| feb0b5f5f5 | |||
| 33fe385052 | |||
| 3b6b12d354 |
+1
-9
@@ -48,16 +48,8 @@ __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
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"lastRun": "2026-03-01T20:42:00+01:00",
|
"lastRun": "2026-03-02T03:55:00Z",
|
||||||
"status": "completed",
|
"status": "blocked",
|
||||||
"phase": "04-workout-modification",
|
"phase": "04-workout-modification",
|
||||||
"activeTask": "04-05-reset-to-original",
|
"milestone": "PHASE_04_COMPLETE",
|
||||||
"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"],
|
"completedTasks": [
|
||||||
"nextTask": "04-06-persistence-improvements",
|
"04-01-database-schema",
|
||||||
"agentSession": "local-exec",
|
"04-02-backend-api",
|
||||||
"agentType": "gravl-pm-cron",
|
"04-03-frontend-edit-mode",
|
||||||
"spawnTime": "2026-03-01T20:42:00+01:00",
|
"04-04-visual-distinction",
|
||||||
"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-05-reset-option",
|
||||||
"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-01-draft-persistence",
|
||||||
"filesModified": [
|
"04-06-02-error-recovery",
|
||||||
"frontend/src/pages/WorkoutSelectPage.jsx",
|
"04-06-03-sync-status-ui"
|
||||||
"frontend/src/App.css",
|
|
||||||
"frontend/src/components/Icons.jsx"
|
|
||||||
],
|
],
|
||||||
"buildStatus": "success",
|
"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.",
|
||||||
"buildTime": "3.59s"
|
"blockReason": "No 04-06-04 spec or 05-* phase defined. Awaiting human direction for next feature.",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,171 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -394,7 +394,7 @@ app.get('/api/today/:programId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(PORT, '0.0.0.0', () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Gravl API running on port ${PORT}`);
|
console.log(`Gravl API running on port ${PORT}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+1
File diff suppressed because one or more lines are too long
-67
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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-hhKetRGz.js"></script>
|
<script type="module" crossorigin src="/assets/index-DIGPkDnZ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-my_lGtI5.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D0xrERyI.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -267,6 +267,24 @@ 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
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
$(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
|
||||||
@@ -405,6 +405,98 @@
|
|||||||
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 {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useRef, useEffect } 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,8 +16,38 @@ 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)
|
||||||
@@ -107,8 +137,19 @@ 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 = {
|
||||||
@@ -126,28 +167,41 @@ 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
|
||||||
@@ -155,6 +209,9 @@ 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 {
|
||||||
@@ -216,6 +273,11 @@ 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
|
||||||
@@ -230,13 +292,9 @@ 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' && (
|
{syncStatus === 'saving' ? 'Sparar...' : 'Spara'}
|
||||||
<>
|
|
||||||
<Icon name="spinner" size={16} /> Sparar...
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{syncStatus !== 'saving' && 'Spara'}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -263,6 +321,23 @@ 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user