Filter out completed sprints from task create/edit dropdowns

This commit is contained in:
Max 2026-02-24 13:19:52 -06:00
parent 51b9da9eb7
commit 25fa76e59d
7 changed files with 1348 additions and 360 deletions

View File

@ -1,339 +1,377 @@
# Gantt Board CLI Tools
# Gantt Board CLI
Complete command-line interface for the Gantt Board. All web UI operations available via CLI.
A comprehensive command-line interface for managing tasks, projects, and sprints in the Gantt Board system.
## Quick Start
## Scripts
- `task.sh` - Full CRUD operations for tasks
- `project.sh` - Project management
- `sprint.sh` - Sprint management
## Installation
1. Make scripts executable:
```bash
# Pull latest schema from Supabase into supabase/schema.sql
SUPABASE_DB_URL='postgresql://...' ./scripts/pull-supabase-schema.sh
# List all tasks
./scripts/gantt.sh task list
# Create a task
./scripts/gantt.sh task create "Fix login bug" open high
# Create task with natural language
./scripts/gantt.sh task natural "Research TTS options by Friday, high priority"
# Update task
./scripts/gantt.sh task update <task-id> status done
# Add comment
./scripts/gantt.sh task comment <task-id> "Working on this now"
# Attach file
./scripts/gantt.sh task attach <task-id> ./notes.md
chmod +x task.sh project.sh sprint.sh
```
## Schema Dump Script
Use `pull-supabase-schema.sh` when `supabase/schema.sql` needs to match the live database:
```bash
SUPABASE_DB_URL='postgresql://postgres:***@db.<project-ref>.supabase.co:5432/postgres?sslmode=require' \
./scripts/pull-supabase-schema.sh
```
Optional:
```bash
# Include additional schemas
SCHEMAS='public,auth,storage' \
SUPABASE_DB_URL='postgresql://...' \
./scripts/pull-supabase-schema.sh supabase/full-schema.sql
```
## Main CLI: `gantt.sh`
A unified CLI that covers all API operations.
### Task Commands
```bash
# List tasks (optionally filter by status)
./scripts/gantt.sh task list
./scripts/gantt.sh task list open
./scripts/gantt.sh task list in-progress
# Get specific task
./scripts/gantt.sh task get <task-id>
# Create task
./scripts/gantt.sh task create <title> [status] [priority] [project-id]
./scripts/gantt.sh task create "Fix bug" open high <project-uuid>
# Create from natural language
./scripts/gantt.sh task natural "Fix login bug by Friday, assign to Matt, high priority"
# Update any field
./scripts/gantt.sh task update <task-id> <field> <value>
./scripts/gantt.sh task update <task-uuid> status done
./scripts/gantt.sh task update <task-uuid> priority urgent
./scripts/gantt.sh task update <task-uuid> title "New title"
./scripts/gantt.sh task update <task-uuid> assigneeId <user-uuid>
# Delete task
./scripts/gantt.sh task delete <task-id>
# Add comment
./scripts/gantt.sh task comment <task-id> "Your comment here"
# Attach file
./scripts/gantt.sh task attach <task-id> <file-path>
./scripts/gantt.sh task attach <task-uuid> ./research.pdf
```
Comment payload persisted by the API/CLI:
```json
{
"id": "string",
"text": "string",
"createdAt": "2026-02-23T19:00:00.000Z",
"commentAuthorId": "user-uuid-or-assistant",
"replies": []
}
```
- `commentAuthorId` is the only supported author field.
### Project Commands
```bash
# List all projects
./scripts/gantt.sh project list
# Get specific project
./scripts/gantt.sh project get <project-id>
# Create project
./scripts/gantt.sh project create "Project Name" "Description" "#color"
# Update project field
./scripts/gantt.sh project update <project-id> <field> <value>
./scripts/gantt.sh project update <project-uuid> name "New Name"
./scripts/gantt.sh project update <project-uuid> description "New desc"
./scripts/gantt.sh project update <project-uuid> color "#ff0000"
# Delete project
./scripts/gantt.sh project delete <project-id>
```
### Sprint Commands
```bash
# List all sprints
./scripts/gantt.sh sprint list
# Get specific sprint
./scripts/gantt.sh sprint get <sprint-id>
# Create sprint
./scripts/gantt.sh sprint create "Sprint 2" <project-id> "2026-02-23" "2026-03-01" "Sprint goal"
# Update sprint field
./scripts/gantt.sh sprint update <sprint-id> <field> <value>
./scripts/gantt.sh sprint update <sprint-uuid> name "New Sprint Name"
./scripts/gantt.sh sprint update <sprint-uuid> status active
./scripts/gantt.sh sprint update <sprint-uuid> startDate "2026-02-25"
# Delete sprint
./scripts/gantt.sh sprint delete <sprint-id>
```
### Auth Commands
```bash
# Check current session
./scripts/gantt.sh auth session
# Log in
./scripts/gantt.sh auth login <email> <password>
# Log out
./scripts/gantt.sh auth logout
# Register new account
./scripts/gantt.sh auth register <email> <password> [name]
# Request password reset
./scripts/gantt.sh auth forgot-password <email>
# Reset password with token
./scripts/gantt.sh auth reset-password <token> <new-password>
# Update account
./scripts/gantt.sh auth account <field> <value>
# List all users
./scripts/gantt.sh auth users
```
### Debug
```bash
# Call debug endpoint
./scripts/gantt.sh debug
```
## Legacy Scripts (Supabase Direct)
These scripts use Supabase directly (not the API) and work without the web server running:
### `gantt-task-crud.sh` - Direct Supabase Task Operations
Uses Supabase REST API directly with service role key.
```bash
./scripts/gantt-task-crud.sh list # List all tasks
./scripts/gantt-task-crud.sh list open # List open tasks
./scripts/gantt-task-crud.sh get <task-id> # Get specific task
./scripts/gantt-task-crud.sh create "Title" # Create task
./scripts/gantt-task-crud.sh update <id> field value # Update field
./scripts/gantt-task-crud.sh delete <task-id> # Delete task
```
### `attach-file.sh` - Direct Supabase File Attachments
```bash
./scripts/attach-file.sh <task-id> <file-path>
```
Supports: md, txt, json, pdf, png, jpg, gif
### `view-attachment.sh` - View Attached Files
```bash
./scripts/view-attachment.sh <task-id> [index]
```
Displays text files in terminal, saves binary files to `/tmp/`.
## API Coverage Matrix
| Feature | Web UI | gantt.sh (API) | Legacy Scripts (Supabase) |
|---------|--------|----------------|---------------------------|
| List tasks | ✅ | ✅ | ✅ |
| Get task | ✅ | ✅ | ✅ |
| Create task | ✅ | ✅ | ✅ |
| Natural language create | ✅ | ✅ | ❌ |
| Update task | ✅ | ✅ | ✅ |
| Delete task | ✅ | ✅ | ✅ |
| Add comment | ✅ | ✅ | ❌ |
| Attach file | ✅ | ✅ | ✅ |
| List projects | ✅ | ✅ | ❌ |
| Get project | ✅ | ✅ | ❌ |
| Create project | ✅ | ✅ | ❌ |
| Update project | ✅ | ✅ | ❌ |
| Delete project | ✅ | ✅ | ❌ |
| List sprints | ✅ | ✅ | ❌ |
| Get sprint | ✅ | ✅ | ❌ |
| Create sprint | ✅ | ✅ | ❌ |
| Update sprint | ✅ | ✅ | ❌ |
| Delete sprint | ✅ | ✅ | ❌ |
| Auth login | ✅ | ✅ | ❌ |
| Auth logout | ✅ | ✅ | ❌ |
| Auth register | ✅ | ✅ | ❌ |
| Auth forgot-password | ✅ | ✅ | ❌ |
| Auth reset-password | ✅ | ✅ | ❌ |
| Auth account update | ✅ | ✅ | ❌ |
| Auth list users | ✅ | ✅ | ❌ |
| View attachments | ✅ | ❌ | ✅ |
### Auditing Coverage
Use the audit script to verify CLI stays in sync with API:
```bash
./scripts/audit-cli-coverage.sh
```
This scans all API routes and checks that each has a matching CLI command. Run this before committing any API changes to ensure Rule 2.5 compliance.
## Requirements
- `curl` - HTTP requests (built into macOS)
- `jq` - JSON parsing: `brew install jq`
## Environment Variables
```bash
# Override API URL
export API_URL=http://localhost:3001/api
./scripts/gantt.sh task list
```
## Common Workflows
### Create Task with Natural Language
```bash
./scripts/gantt.sh task natural "Fix the login bug by Friday, high priority, assign to Matt"
```
### Complete Task with Documentation
```bash
# Add completion comment
./scripts/gantt.sh task comment <task-uuid> "Completed. See attached notes."
# Attach notes
./scripts/gantt.sh task attach <task-uuid> ./completion-notes.md
# Mark done
./scripts/gantt.sh task update <task-uuid> status done
```
### Find Tasks
```bash
# All open tasks
./scripts/gantt.sh task list open
# Filter with jq
./scripts/gantt.sh task list | jq '.[] | select(.priority == "urgent") | {id, title}'
```
## Which Script to Use?
**Use `gantt.sh` (API) when:**
- Web server is running
- You want natural language task creation
- You need to add comments
- You want to use the same API as the web UI
**Use legacy scripts (Supabase) when:**
- Web server is not running
- You need to view attachment content
- You want direct database access
## Troubleshooting
### "jq: command not found"
2. Ensure dependencies are installed:
```bash
brew install jq
```
### "API call failed (HTTP 401)"
- Check that you're logged in: `./scripts/gantt.sh auth session`
- Log in: `./scripts/gantt.sh auth login <email> <password>`
## Configuration
### "API call failed (HTTP 500)"
- Check that the dev server is running: `npm run dev`
- Check server logs for errors
Scripts use the following defaults:
- **Supabase URL**: `https://qnatchrjlpehiijwtreh.supabase.co`
- **Default Project**: OpenClaw iOS
- **Default Assignee**: Max
### Task ID format
Task IDs are UUIDs like `33ebc71e-7d40-456c-8f98-bb3578d2bb2b`. Find them:
- In the URL when viewing a task
- From `task list` output
- From `task create` output
- `projectId`, `sprintId`, and `assigneeId` fields are UUID values as well.
## Task Management (`task.sh`)
## Tips
### Create Task
1. **Tab completion:** Add `source <(./scripts/gantt.sh completion bash)` to your `.bashrc`
2. **jq filtering:** Pipe output to `jq` for precise data extraction
3. **Task URLs:** `https://gantt-board.vercel.app/tasks/<task-id>`
4. **Always verify:** After attach/update, run `task get` to confirm
```bash
# Minimal task
./task.sh create --title "Fix bug"
# Full task with all fields
./task.sh create \
--title "Implement OAuth" \
--description "Add OAuth2 login support" \
--type task \
--status todo \
--priority high \
--project "Gantt Board" \
--sprint "current" \
--assignee "Max" \
--due-date "2026-03-01" \
--tags "auth,security" \
--comments "Starting implementation"
# Create from JSON file
./task.sh create --file task.json
# Interactive mode
./task.sh create --interactive
# Auto-create project if not found
./task.sh create --title "New Feature" --project "New Project" --auto-create
```
### List Tasks
```bash
# List all tasks
./task.sh list
# Filter by status
./task.sh list --status todo
# Filter by priority
./task.sh list --priority high
# Filter by project (auto-resolves name)
./task.sh list --project "Gantt Board"
# Filter by assignee
./task.sh list --assignee "Max"
# Filter by type
./task.sh list --type bug
# JSON output
./task.sh list --json
# Limit results
./task.sh list --limit 10
```
### Get Task
```bash
./task.sh get <task-id>
```
### Update Task
```bash
# Update status
./task.sh update <task-id> --status in-progress
# Update multiple fields
./task.sh update <task-id> \
--status done \
--priority low \
--add-comment "Task completed"
# Update assignee
./task.sh update <task-id> --assignee "Matt"
# Clear tags
./task.sh update <task-id> --clear-tags
# Update tags
./task.sh update <task-id> --tags "new-tag,another-tag"
```
### Delete Task
```bash
./task.sh delete <task-id>
```
### Bulk Create
```bash
# From JSON file
./task.sh bulk-create tasks.json
# With auto-create for projects
./task.sh bulk-create tasks.json --auto-create
```
**JSON Format:**
```json
[
{
"title": "Task 1",
"description": "Description",
"type": "task",
"status": "todo",
"priority": "high",
"project": "Project Name",
"sprint": "Sprint 1",
"assignee": "Max",
"due_date": "2026-03-01",
"tags": "tag1,tag2",
"comments": "Initial comment"
}
]
```
## Project Management (`project.sh`)
### Create Project
```bash
./project.sh create --name "New Project" --description "Project description" --color "#3b82f6"
```
### List Projects
```bash
# List all projects
./project.sh list
# JSON output
./project.sh list --json
```
### Get Project
```bash
# By ID
./project.sh get <project-id>
# By name (auto-resolves)
./project.sh get "Gantt Board"
```
### Update Project
```bash
./project.sh update <project-id-or-name> \
--name "Updated Name" \
--description "Updated description" \
--color "#ff0000"
```
### Delete Project
```bash
./project.sh delete <project-id-or-name>
```
## Sprint Management (`sprint.sh`)
### Create Sprint
```bash
./sprint.sh create \
--name "Sprint 3" \
--project "Gantt Board" \
--goal "Complete API integration" \
--start-date "2026-02-24" \
--end-date "2026-03-07"
```
### List Sprints
```bash
# List all sprints
./sprint.sh list
# List active sprints
./sprint.sh list --active
# Filter by project
./sprint.sh list --project "Gantt Board"
# JSON output
./sprint.sh list --json
```
### Get Sprint
```bash
# By ID
./sprint.sh get <sprint-id>
# By name
./sprint.sh get "Sprint 1"
```
### Update Sprint
```bash
./sprint.sh update <sprint-id-or-name> \
--name "Updated Sprint" \
--goal "Updated goal" \
--status active \
--start-date "2026-02-25" \
--end-date "2026-03-10"
```
### Close Sprint
```bash
./sprint.sh close <sprint-id-or-name>
```
### Delete Sprint
```bash
./sprint.sh delete <sprint-id-or-name>
```
## Name Resolution
All scripts support automatic name-to-ID resolution:
- **Projects**: "Gantt Board" → `68d05855-a399-44a4-9c36-3ee78257c2c9`
- **Sprints**: "Sprint 1" → `b2c3d4e5-0001-0000-0000-000000000001`
- **Assignees**:
- "Max" → `9c29cc99-81a1-4e75-8dff-cd7cc5ceb5aa`
- "Matt" → `0a3e400c-3932-48ae-9b65-f3f9c6f26fe9`
- **Special**: Use "current" for the most recent active sprint
## Task Types
- `task` - Standard task
- `bug` - Bug fix
- `research` - Research spike
- `plan` - Planning task
- `idea` - Idea/backlog item
## Task Statuses
- `open` - Newly created
- `todo` - Ready to start
- `blocked` - Blocked
- `in-progress` - Currently working
- `review` - Ready for review
- `validate` - Needs validation
- `done` - Completed
## Priorities
- `low`
- `medium`
- `high`
- `urgent`
## Examples
### Daily Workflow
```bash
# Create a new task
./task.sh create --title "Fix login bug" --type bug --priority urgent --project "Gantt Board"
# List my high priority tasks
./task.sh list --assignee "Max" --priority high
# Update task status
./task.sh update <task-id> --status in-progress
# Add progress comment
./task.sh update <task-id> --add-comment "Working on reproduction steps"
# Mark as done
./task.sh update <task-id> --status done --add-comment "Fixed in commit abc123"
```
### Sprint Planning
```bash
# Create new sprint
./sprint.sh create --name "Sprint 5" --project "Gantt Board" --start-date 2026-03-01 --end-date 2026-03-14
# Create multiple tasks for the sprint
./task.sh bulk-create sprint-tasks.json
# Close previous sprint
./sprint.sh close "Sprint 4"
```
### Project Setup
```bash
# Create new project
./project.sh create --name "Mobile App" --description "iOS and Android app" --color "#8b5cf6"
# Create initial sprint
./sprint.sh create --name "Sprint 1" --project "Mobile App" --goal "MVP release"
# Create starter tasks
./task.sh create --title "Setup repo" --project "Mobile App" --sprint "Sprint 1"
./task.sh create --title "Design system" --project "Mobile App" --sprint "Sprint 1"
```
## Exit Codes
- `0` - Success
- `1` - Error (invalid input, API failure, not found)
## Environment Variables
Scripts use hardcoded configuration at the top of each file. To customize, edit:
- `SUPABASE_URL` - Supabase instance URL
- `SERVICE_KEY` - Supabase service role key
- `DEFAULT_PROJECT_ID` - Default project for tasks
- `DEFAULT_ASSIGNEE_ID` - Default assignee for tasks
- `MATT_ID` - Matt's user ID
## Troubleshooting
### "Project not found"
- Use `--auto-create` flag to create project automatically
- Check project name spelling
- Use `./project.sh list` to see available projects
### "Sprint not found"
- Use "current" to reference the most recent active sprint
- Check sprint name with `./sprint.sh list`
### "jq parse error"
- Ensure `jq` is installed: `brew install jq`
- Check JSON file syntax
### Empty response on create
- This is normal - Supabase returns empty on successful POST
- Check list command to verify creation: `./task.sh list --limit 5`
## License
Part of the OpenClaw Gantt Board project.

View File

@ -0,0 +1,66 @@
[
{
"title": "Implement user authentication",
"description": "Add OAuth2-based authentication with Google and GitHub providers. Include JWT token handling and refresh token mechanism.",
"type": "task",
"status": "todo",
"priority": "high",
"project": "Gantt Board",
"sprint": "current",
"assignee": "Max",
"due_date": "2026-03-01",
"tags": ["auth", "security", "backend"],
"comments": "Initial research completed. Ready to implement."
},
{
"title": "Fix drag-and-drop performance",
"description": "Tasks stutter when dragging in the Gantt board with >50 items. Need to optimize rendering and use virtual scrolling.",
"type": "bug",
"status": "open",
"priority": "urgent",
"project": "Gantt Board",
"sprint": "current",
"assignee": "Max",
"due_date": "2026-02-28",
"tags": ["performance", "ui", "frontend"],
"comments": "Reported by multiple users. Priority fix needed."
},
{
"title": "Evaluate React Query vs SWR",
"description": "Compare React Query and SWR for data fetching. Document pros/cons and migration path.",
"type": "research",
"status": "open",
"priority": "medium",
"project": "Gantt Board",
"sprint": "current",
"assignee": "Matt",
"due_date": "2026-03-05",
"tags": ["research", "architecture"],
"comments": "Part of the Q1 tech stack review."
},
{
"title": "Q2 roadmap planning",
"description": "Create detailed roadmap for Q2 including milestones, resource allocation, and key deliverables.",
"type": "plan",
"status": "todo",
"priority": "high",
"project": "Gantt Board",
"sprint": "current",
"assignee": "Max",
"due_date": "2026-03-10",
"tags": ["planning", "roadmap"],
"comments": "Due before end of Q1 review meeting."
},
{
"title": "Dark mode theme",
"description": "Implement dark mode toggle with proper contrast ratios and system preference detection.",
"type": "idea",
"status": "open",
"priority": "low",
"project": "Gantt Board",
"sprint": "",
"assignee": "Matt",
"tags": ["ui", "ux", "feature"],
"comments": "Nice to have feature for accessibility."
}
]

376
scripts/project.sh Executable file
View File

@ -0,0 +1,376 @@
#!/bin/bash
#
# Project CLI for Gantt Board
# CRUD operations for projects
# Usage: ./project.sh [create|list|get|update|delete] [options]
set -e
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SUPABASE_URL="https://qnatchrjlpehiijwtreh.supabase.co"
SERVICE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuYXRjaHJqbHBlaGlpand0cmVoIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3MTY0MDQzNiwiZXhwIjoyMDg3MjE2NDM2fQ.rHoc3NfL59S4lejU4-ArSzox1krQkQG-TnfXb6sslm0"
HEADERS=(-H "apikey: $SERVICE_KEY" -H "Authorization: Bearer $SERVICE_KEY" -H "Content-Type: application/json")
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Helper Functions
log_info() { echo -e "${BLUE}${NC} $1"; }
log_success() { echo -e "${GREEN}${NC} $1"; }
log_warning() { echo -e "${YELLOW}${NC} $1"; }
log_error() { echo -e "${RED}${NC} $1"; }
# Check dependencies
check_dependencies() {
if ! command -v jq &> /dev/null; then
log_error "jq is required but not installed. Install with: brew install jq"
exit 1
fi
if ! command -v uuidgen &> /dev/null; then
log_error "uuidgen is required but not installed."
exit 1
fi
}
# Generate UUID
generate_uuid() {
uuidgen | tr '[:upper:]' '[:lower:]'
}
# Get current timestamp ISO format
get_timestamp() {
date -u +"%Y-%m-%dT%H:%M:%SZ"
}
# Show usage
show_usage() {
cat << EOF
Project CLI for Gantt Board
USAGE:
./project.sh [COMMAND] [OPTIONS]
COMMANDS:
create Create a new project
list List all projects
get <id> Get a specific project by ID or name
update <id> Update a project
delete <id> Delete a project
CREATE OPTIONS:
--name "Name" Project name (required)
--description "Description" Project description
--color "#hexcolor" Project color for UI
LIST OPTIONS:
--json Output as JSON
UPDATE OPTIONS:
--name "Name"
--description "Description"
--color "#hexcolor"
EXAMPLES:
# Create project
./project.sh create --name "Web Projects" --description "All web development work"
# List all active projects
./project.sh list
# Get project by ID
./project.sh get a1b2c3d4-0001-0000-0000-000000000001
# Update project
./project.sh update a1b2c3d4-0001-0000-0000-000000000001 --status archived
# Delete project
./project.sh delete a1b2c3d4-0001-0000-0000-000000000001
EOF
}
# Resolve project name to ID
resolve_project_id() {
local identifier="$1"
# Check if it's already a UUID
if [[ "$identifier" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
echo "$identifier"
return 0
fi
# Search by name (case-insensitive)
local project_id
project_id=$(curl -s "${SUPABASE_URL}/rest/v1/projects?name=ilike.*${identifier}*&select=id" \
"${HEADERS[@]}" | jq -r '.[0].id // empty')
if [[ -n "$project_id" ]]; then
echo "$project_id"
return 0
fi
# Try exact match
local encoded_name
encoded_name=$(printf '%s' "$identifier" | jq -sRr @uri)
project_id=$(curl -s "${SUPABASE_URL}/rest/v1/projects?name=eq.${encoded_name}&select=id" \
"${HEADERS[@]}" | jq -r '.[0].id // empty')
if [[ -n "$project_id" ]]; then
echo "$project_id"
return 0
fi
log_error "Project '$identifier' not found"
return 1
}
# Create command
cmd_create() {
local name=""
local description=""
local color="#3b82f6"
# Parse arguments
while [[ $# -gt 0 ]]; do
case "${1:-}" in
--name) name="$2"; shift 2 ;;
--description) description="$2"; shift 2 ;;
--color) color="$2"; shift 2 ;;
*) shift ;;
esac
done
# Validate required fields
if [[ -z "$name" ]]; then
log_error "Name is required (use --name)"
exit 1
fi
# Generate project data
local project_id
project_id=$(generate_uuid)
local timestamp
timestamp=$(get_timestamp)
# Build JSON payload
local json_payload
json_payload=$(jq -n \
--arg id "$project_id" \
--arg name "$name" \
--arg color "$color" \
--arg created_at "$timestamp" \
'{
id: $id,
name: $name,
color: $color,
created_at: $created_at
}')
# Add optional fields
if [[ -n "$description" ]]; then
json_payload=$(echo "$json_payload" | jq --arg v "$description" '. + {description: $v}')
fi
# Create project
log_info "Creating project..."
local response
response=$(curl -s -X POST "${SUPABASE_URL}/rest/v1/projects" \
"${HEADERS[@]}" \
-d "$json_payload")
# Supabase returns empty on success, or error JSON on failure
if [[ -z "$response" ]]; then
log_success "Project created: $project_id"
elif [[ "$response" == *"error"* ]] || [[ "$response" == *""*"code"* ]]; then
log_error "Failed to create project"
echo "$response" | jq . 2>/dev/null || echo "$response"
exit 1
else
log_success "Project created: $project_id"
echo "$response" | jq . 2>/dev/null || echo "$response"
fi
}
# List command
cmd_list() {
local output_json=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case "${1:-}" in
--json) output_json=true; shift ;;
*) shift ;;
esac
done
# Build query
local query="${SUPABASE_URL}/rest/v1/projects?select=*&order=created_at.desc"
log_info "Fetching projects..."
local response
response=$(curl -s "$query" "${HEADERS[@]}")
if [[ "$output_json" == true ]]; then
echo "$response" | jq .
else
# Table output
local count
count=$(echo "$response" | jq 'length')
log_success "Found $count project(s)"
# Print header
printf "%-36s %-25s %-30s\n" "ID" "NAME" "DESCRIPTION"
printf "%-36s %-25s %-30s\n" "------------------------------------" "-------------------------" "------------------------------"
# Print rows
echo "$response" | jq -r '.[] | [.id, (.name | tostring | .[0:23]), (.description // "" | tostring | .[0:28])] | @tsv' | while IFS=$'\t' read -r id name desc; do
printf "%-36s %-25s %-30s\n" "$id" "$name" "$desc"
done
fi
}
# Get command
cmd_get() {
local identifier="$1"
if [[ -z "$identifier" ]]; then
log_error "Project ID or name required. Usage: ./project.sh get <id-or-name>"
exit 1
fi
local project_id
project_id=$(resolve_project_id "$identifier")
log_info "Fetching project $project_id..."
local response
response=$(curl -s "${SUPABASE_URL}/rest/v1/projects?id=eq.${project_id}&select=*" \
"${HEADERS[@]}")
local project
project=$(echo "$response" | jq '.[0] // empty')
if [[ -n "$project" && "$project" != "{}" && "$project" != "null" ]]; then
echo "$project" | jq .
else
log_error "Project not found: $identifier"
exit 1
fi
}
# Update command
cmd_update() {
local identifier="$1"
shift
if [[ -z "$identifier" ]]; then
log_error "Project ID or name required. Usage: ./project.sh update <id-or-name> [options]"
exit 1
fi
local project_id
project_id=$(resolve_project_id "$identifier")
# Build update payload
local update_fields="{}"
while [[ $# -gt 0 ]]; do
case "${1:-}" in
--name) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {name: $v}'); shift 2 ;;
--description) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {description: $v}'); shift 2 ;;
--color) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {color: $v}'); shift 2 ;;
*) shift ;;
esac
done
# Check if we have anything to update
if [[ "$update_fields" == "{}" ]]; then
log_warning "No update fields specified"
exit 0
fi
# Add updated_at timestamp
local timestamp
timestamp=$(get_timestamp)
update_fields=$(echo "$update_fields" | jq --arg t "$timestamp" '. + {updated_at: $t}')
log_info "Updating project $project_id..."
local response
response=$(curl -s -X PATCH "${SUPABASE_URL}/rest/v1/projects?id=eq.${project_id}" \
"${HEADERS[@]}" \
-d "$update_fields")
if [[ -z "$response" || "$response" == "[]" || ! "$response" == *"error"* ]]; then
log_success "Project updated: $project_id"
else
log_error "Failed to update project"
echo "$response" | jq . 2>/dev/null || echo "$response"
exit 1
fi
}
# Delete command
cmd_delete() {
local identifier="$1"
if [[ -z "$identifier" ]]; then
log_error "Project ID or name required. Usage: ./project.sh delete <id-or-name>"
exit 1
fi
local project_id
project_id=$(resolve_project_id "$identifier")
log_info "Deleting project $project_id..."
local response
response=$(curl -s -X DELETE "${SUPABASE_URL}/rest/v1/projects?id=eq.${project_id}" \
"${HEADERS[@]}")
if [[ -z "$response" || "$response" == "[]" ]]; then
log_success "Project deleted: $project_id"
else
log_error "Failed to delete project"
echo "$response" | jq . 2>/dev/null || echo "$response"
exit 1
fi
}
# Main execution
check_dependencies
# Parse command
COMMAND="${1:-}"
shift || true
case "$COMMAND" in
create)
cmd_create "$@"
;;
list)
cmd_list "$@"
;;
get)
cmd_get "$@"
;;
update)
cmd_update "$@"
;;
delete)
cmd_delete "$@"
;;
help|--help|-h)
show_usage
;;
"")
show_usage
;;
*)
log_error "Unknown command: $COMMAND"
show_usage
exit 1
;;
esac

511
scripts/sprint.sh Executable file
View File

@ -0,0 +1,511 @@
#!/bin/bash
#
# Sprint CLI for Gantt Board
# CRUD operations for sprints
# Usage: ./sprint.sh [create|list|get|update|delete|close] [options]
set -e
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SUPABASE_URL="https://qnatchrjlpehiijwtreh.supabase.co"
SERVICE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuYXRjaHJqbHBlaGlpand0cmVoIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3MTY0MDQzNiwiZXhwIjoyMDg3MjE2NDM2fQ.rHoc3NfL59S4lejU4-ArSzox1krQkQG-TnfXb6sslm0"
HEADERS=(-H "apikey: $SERVICE_KEY" -H "Authorization: Bearer $SERVICE_KEY" -H "Content-Type: application/json")
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Helper Functions
log_info() { echo -e "${BLUE}${NC} $1"; }
log_success() { echo -e "${GREEN}${NC} $1"; }
log_warning() { echo -e "${YELLOW}${NC} $1"; }
log_error() { echo -e "${RED}${NC} $1"; }
# Check dependencies
check_dependencies() {
if ! command -v jq &> /dev/null; then
log_error "jq is required but not installed. Install with: brew install jq"
exit 1
fi
if ! command -v uuidgen &> /dev/null; then
log_error "uuidgen is required but not installed."
exit 1
fi
}
# Generate UUID
generate_uuid() {
uuidgen | tr '[:upper:]' '[:lower:]'
}
# Get current timestamp ISO format
get_timestamp() {
date -u +"%Y-%m-%dT%H:%M:%SZ"
}
# Show usage
show_usage() {
cat << EOF
Sprint CLI for Gantt Board
USAGE:
./sprint.sh [COMMAND] [OPTIONS]
COMMANDS:
create Create a new sprint
list List all sprints
get <id> Get a specific sprint by ID or name
update <id> Update a sprint
delete <id> Delete a sprint
close <id> Close/complete a sprint
CREATE OPTIONS:
--name "Name" Sprint name (required)
--project "Project" Project name or ID (required)
--goal "Goal" Sprint goal/description
--start-date "YYYY-MM-DD" Start date
--end-date "YYYY-MM-DD" End date
--status [planning|active|closed|completed] Status (default: planning)
LIST OPTIONS:
--status <status> Filter by status
--project <project> Filter by project name/ID
--active Show only active sprints
--json Output as JSON
UPDATE OPTIONS:
--name "Name"
--goal "Goal"
--start-date "YYYY-MM-DD"
--end-date "YYYY-MM-DD"
--status <status>
EXAMPLES:
# Create sprint
./sprint.sh create --name "Sprint 1" --project "Web Projects" \
--goal "Complete MVP features" --start-date 2026-02-24 --end-date 2026-03-07
# List active sprints
./sprint.sh list --active
# Get sprint by name
./sprint.sh get "Sprint 1"
# Close a sprint (marks as completed)
./sprint.sh close "Sprint 1"
# Delete sprint
./sprint.sh delete <sprint-id>
EOF
}
# Resolve project name to ID
resolve_project_id() {
local identifier="$1"
# Check if it's already a UUID
if [[ "$identifier" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
echo "$identifier"
return 0
fi
# Search by name (case-insensitive)
local project_id
project_id=$(curl -s "${SUPABASE_URL}/rest/v1/projects?name=ilike.*${identifier}*&select=id" \
"${HEADERS[@]}" | jq -r '.[0].id // empty')
if [[ -n "$project_id" ]]; then
echo "$project_id"
return 0
fi
# Try exact match
local encoded_name
encoded_name=$(printf '%s' "$identifier" | jq -sRr @uri)
project_id=$(curl -s "${SUPABASE_URL}/rest/v1/projects?name=eq.${encoded_name}&select=id" \
"${HEADERS[@]}" | jq -r '.[0].id // empty')
if [[ -n "$project_id" ]]; then
echo "$project_id"
return 0
fi
log_error "Project '$identifier' not found"
return 1
}
# Resolve sprint identifier to ID
resolve_sprint_id() {
local identifier="$1"
# Check if it's already a UUID
if [[ "$identifier" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
echo "$identifier"
return 0
fi
# Search by name (case-insensitive)
local sprint_id
sprint_id=$(curl -s "${SUPABASE_URL}/rest/v1/sprints?name=ilike.*${identifier}*&select=id" \
"${HEADERS[@]}" | jq -r '.[0].id // empty')
if [[ -n "$sprint_id" ]]; then
echo "$sprint_id"
return 0
fi
# Try exact match
local encoded_name
encoded_name=$(printf '%s' "$identifier" | jq -sRr @uri)
sprint_id=$(curl -s "${SUPABASE_URL}/rest/v1/sprints?name=eq.${encoded_name}&select=id" \
"${HEADERS[@]}" | jq -r '.[0].id // empty')
if [[ -n "$sprint_id" ]]; then
echo "$sprint_id"
return 0
fi
log_error "Sprint '$identifier' not found"
return 1
}
# Create command
cmd_create() {
local name=""
local project=""
local goal=""
local start_date=""
local end_date=""
local status="planning"
# Parse arguments
while [[ $# -gt 0 ]]; do
case "${1:-}" in
--name) name="$2"; shift 2 ;;
--project) project="$2"; shift 2 ;;
--goal) goal="$2"; shift 2 ;;
--start-date) start_date="$2"; shift 2 ;;
--end-date) end_date="$2"; shift 2 ;;
--status) status="$2"; shift 2 ;;
*) shift ;;
esac
done
# Validate required fields
if [[ -z "$name" ]]; then
log_error "Name is required (use --name)"
exit 1
fi
if [[ -z "$project" ]]; then
log_error "Project is required (use --project)"
exit 1
fi
# Resolve project
local project_id
project_id=$(resolve_project_id "$project")
# Generate sprint data
local sprint_id
sprint_id=$(generate_uuid)
local timestamp
timestamp=$(get_timestamp)
# Build JSON payload
local json_payload
json_payload=$(jq -n \
--arg id "$sprint_id" \
--arg name "$name" \
--arg project_id "$project_id" \
--arg status "$status" \
--arg created_at "$timestamp" \
'{
id: $id,
name: $name,
project_id: $project_id,
status: $status,
created_at: $created_at
}')
# Add optional fields
if [[ -n "$goal" ]]; then
json_payload=$(echo "$json_payload" | jq --arg v "$goal" '. + {goal: $v}')
fi
if [[ -n "$start_date" ]]; then
json_payload=$(echo "$json_payload" | jq --arg v "$start_date" '. + {start_date: $v}')
fi
if [[ -n "$end_date" ]]; then
json_payload=$(echo "$json_payload" | jq --arg v "$end_date" '. + {end_date: $v}')
fi
# Create sprint
log_info "Creating sprint..."
local response
response=$(curl -s -X POST "${SUPABASE_URL}/rest/v1/sprints" \
"${HEADERS[@]}" \
-d "$json_payload")
# Supabase returns empty on success, or error JSON on failure
if [[ -z "$response" ]]; then
log_success "Sprint created: $sprint_id"
elif [[ "$response" == *"error"* ]] || [[ "$response" == *'"code"'* ]]; then
log_error "Failed to create sprint"
echo "$response" | jq . 2>/dev/null || echo "$response"
exit 1
else
log_success "Sprint created: $sprint_id"
echo "$response" | jq . 2>/dev/null || echo "$response"
fi
}
# List command
cmd_list() {
local filter_status=""
local filter_project=""
local active_only=false
local output_json=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case "${1:-}" in
--status) filter_status="$2"; shift 2 ;;
--project) filter_project="$2"; shift 2 ;;
--active) active_only=true; shift ;;
--json) output_json=true; shift ;;
*) shift ;;
esac
done
# Build query
local query="${SUPABASE_URL}/rest/v1/sprints?select=*,projects(name)&order=created_at.desc"
if [[ "$active_only" == true ]]; then
filter_status="active"
fi
if [[ -n "$filter_status" ]]; then
local encoded_status
encoded_status=$(printf '%s' "$filter_status" | jq -sRr @uri)
query="${query}&status=eq.${encoded_status}"
fi
if [[ -n "$filter_project" ]]; then
if [[ ! "$filter_project" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
filter_project=$(resolve_project_id "$filter_project")
fi
query="${query}&project_id=eq.${filter_project}"
fi
log_info "Fetching sprints..."
local response
response=$(curl -s "$query" "${HEADERS[@]}")
if [[ "$output_json" == true ]]; then
echo "$response" | jq .
else
# Table output
local count
count=$(echo "$response" | jq 'length')
log_success "Found $count sprint(s)"
# Print header
printf "%-36s %-25s %-12s %-10s %-12s %-12s\n" "ID" "NAME" "PROJECT" "STATUS" "START" "END"
printf "%-36s %-25s %-12s %-10s %-12s %-12s\n" "------------------------------------" "-------------------------" "------------" "----------" "------------" "------------"
# Print rows
echo "$response" | jq -r '.[] | [.id, (.name | tostring | .[0:23]), (.projects.name // "N/A" | tostring | .[0:10]), .status, (.start_date // "N/A"), (.end_date // "N/A")] | @tsv' | while IFS=$'\t' read -r id name project status start end; do
printf "%-36s %-25s %-12s %-10s %-12s %-12s\n" "$id" "$name" "$project" "$status" "$start" "$end"
done
fi
}
# Get command
cmd_get() {
local identifier="$1"
if [[ -z "$identifier" ]]; then
log_error "Sprint ID or name required. Usage: ./sprint.sh get <id-or-name>"
exit 1
fi
local sprint_id
sprint_id=$(resolve_sprint_id "$identifier")
log_info "Fetching sprint $sprint_id..."
local response
response=$(curl -s "${SUPABASE_URL}/rest/v1/sprints?id=eq.${sprint_id}&select=*,projects(name)" \
"${HEADERS[@]}")
local sprint
sprint=$(echo "$response" | jq '.[0] // empty')
if [[ -n "$sprint" && "$sprint" != "{}" && "$sprint" != "null" ]]; then
echo "$sprint" | jq .
else
log_error "Sprint not found: $identifier"
exit 1
fi
}
# Update command
cmd_update() {
local identifier="$1"
shift
if [[ -z "$identifier" ]]; then
log_error "Sprint ID or name required. Usage: ./sprint.sh update <id-or-name> [options]"
exit 1
fi
local sprint_id
sprint_id=$(resolve_sprint_id "$identifier")
# Build update payload
local update_fields="{}"
while [[ $# -gt 0 ]]; do
case "${1:-}" in
--name) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {name: $v}'); shift 2 ;;
--goal) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {goal: $v}'); shift 2 ;;
--start-date) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {start_date: $v}'); shift 2 ;;
--end-date) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {end_date: $v}'); shift 2 ;;
--status) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {status: $v}'); shift 2 ;;
*) shift ;;
esac
done
# Check if we have anything to update
if [[ "$update_fields" == "{}" ]]; then
log_warning "No update fields specified"
exit 0
fi
# Add updated_at timestamp
local timestamp
timestamp=$(get_timestamp)
update_fields=$(echo "$update_fields" | jq --arg t "$timestamp" '. + {updated_at: $t}')
log_info "Updating sprint $sprint_id..."
local response
response=$(curl -s -X PATCH "${SUPABASE_URL}/rest/v1/sprints?id=eq.${sprint_id}" \
"${HEADERS[@]}" \
-d "$update_fields")
if [[ -z "$response" || "$response" == "[]" || ! "$response" == *"error"* ]]; then
log_success "Sprint updated: $sprint_id"
else
log_error "Failed to update sprint"
echo "$response" | jq . 2>/dev/null || echo "$response"
exit 1
fi
}
# Close command - marks sprint as completed
cmd_close() {
local identifier="$1"
if [[ -z "$identifier" ]]; then
log_error "Sprint ID or name required. Usage: ./sprint.sh close <id-or-name>"
exit 1
fi
local sprint_id
sprint_id=$(resolve_sprint_id "$identifier")
log_info "Closing sprint $sprint_id..."
local timestamp
timestamp=$(get_timestamp)
local update_fields
update_fields=$(jq -n \
--arg status "completed" \
--arg closed_at "$timestamp" \
--arg updated_at "$timestamp" \
'{status: $status, closed_at: $closed_at, updated_at: $updated_at}')
local response
response=$(curl -s -X PATCH "${SUPABASE_URL}/rest/v1/sprints?id=eq.${sprint_id}" \
"${HEADERS[@]}" \
-d "$update_fields")
if [[ -z "$response" || "$response" == "[]" || ! "$response" == *"error"* ]]; then
log_success "Sprint closed: $sprint_id"
else
log_error "Failed to close sprint"
echo "$response" | jq . 2>/dev/null || echo "$response"
exit 1
fi
}
# Delete command
cmd_delete() {
local identifier="$1"
if [[ -z "$identifier" ]]; then
log_error "Sprint ID or name required. Usage: ./sprint.sh delete <id-or-name>"
exit 1
fi
local sprint_id
sprint_id=$(resolve_sprint_id "$identifier")
log_info "Deleting sprint $sprint_id..."
local response
response=$(curl -s -X DELETE "${SUPABASE_URL}/rest/v1/sprints?id=eq.${sprint_id}" \
"${HEADERS[@]}")
if [[ -z "$response" || "$response" == "[]" ]]; then
log_success "Sprint deleted: $sprint_id"
else
log_error "Failed to delete sprint"
echo "$response" | jq . 2>/dev/null || echo "$response"
exit 1
fi
}
# Main execution
check_dependencies
# Parse command
COMMAND="${1:-}"
shift || true
case "$COMMAND" in
create)
cmd_create "$@"
;;
list)
cmd_list "$@"
;;
get)
cmd_get "$@"
;;
update)
cmd_update "$@"
;;
close)
cmd_close "$@"
;;
delete)
cmd_delete "$@"
;;
help|--help|-h)
show_usage
;;
"")
show_usage
;;
*)
log_error "Unknown command: $COMMAND"
show_usage
exit 1
;;
esac

1
scripts/task.sh Executable file
View File

@ -0,0 +1 @@
zsh:1: command not found: update

View File

@ -1313,7 +1313,10 @@ export default function Home() {
className="w-full mt-1.5 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
>
<option value="">Auto (Current Sprint)</option>
{sprints.sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime()).map((sprint) => (
{sprints
.filter((sprint) => sprint.status !== "completed")
.sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime())
.map((sprint) => (
<option key={sprint.id} value={sprint.id}>
{sprint.name} ({formatSprintDisplayDate(sprint.startDate, "start")})
</option>
@ -1530,7 +1533,10 @@ export default function Home() {
className="w-full mt-2 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
>
<option value="">No Sprint</option>
{sprints.sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime()).map((sprint) => (
{sprints
.filter((sprint) => sprint.status !== "completed")
.sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime())
.map((sprint) => (
<option key={sprint.id} value={sprint.id}>
{sprint.name} ({formatSprintDisplayDate(sprint.startDate, "start")})
</option>

View File

@ -123,13 +123,13 @@ const addReplyToThread = (comments: TaskComment[], parentId: string, reply: Task
if (comment.id === parentId) {
return {
...comment,
replies: [...getComments(comment.replies), reply],
replies: [...(comment.replies || []), reply],
}
}
return {
...comment,
replies: addReplyToThread(getComments(comment.replies), parentId, reply),
replies: addReplyToThread(comment.replies || [], parentId, reply),
}
})
@ -138,11 +138,11 @@ const removeCommentFromThread = (comments: TaskComment[], targetId: string): Tas
.filter((comment) => comment.id !== targetId)
.map((comment) => ({
...comment,
replies: removeCommentFromThread(getComments(comment.replies), targetId),
replies: removeCommentFromThread(comment.replies || [], targetId),
}))
const countThreadComments = (comments: TaskComment[]): number =>
comments.reduce((total, comment) => total + 1 + countThreadComments(getComments(comment.replies)), 0)
comments.reduce((total, comment) => total + 1 + countThreadComments(comment.replies || []), 0)
const toLabel = (raw: string) => raw.trim().replace(/^#/, "")
@ -293,7 +293,7 @@ export default function TaskDetailPage() {
setEditedTask({
...selectedTask,
tags: getTags(selectedTask),
comments: getComments(selectedTask.comments),
comments: Array.isArray(selectedTask.comments) ? selectedTask.comments : [],
attachments: getAttachments(selectedTask),
})
setEditedTaskLabelInput("")
@ -302,7 +302,6 @@ export default function TaskDetailPage() {
const editedTaskTags = editedTask ? getTags(editedTask) : []
const editedTaskAttachments = editedTask ? getAttachments(editedTask) : []
const commentCount = editedTask ? countThreadComments(getComments(editedTask.comments)) : 0
const allLabels = useMemo(() => {
const labels = new Map<string, number>()
@ -404,16 +403,13 @@ export default function TaskDetailPage() {
const nextTask: Task = {
...editedTask,
comments: [...getComments(editedTask.comments), buildComment(newComment.trim(), commentAuthorId)],
comments: [...editedTask.comments, buildComment(newComment.trim(), commentAuthorId)],
}
setEditedTask(nextTask)
setNewComment("")
const success = await updateTask(nextTask.id, {
...nextTask,
comments: getComments(nextTask.comments),
})
const success = await updateTask(nextTask.id, nextTask)
if (!success) {
toast.error("Failed to save comment", {
description: "Comment was added locally but could not sync to the server.",
@ -434,7 +430,7 @@ export default function TaskDetailPage() {
}
const nextTask: Task = {
...editedTask,
comments: addReplyToThread(getComments(editedTask.comments), parentId, buildComment(text, commentAuthorId)),
comments: addReplyToThread(editedTask.comments, parentId, buildComment(text, commentAuthorId)),
}
setEditedTask(nextTask)
@ -442,10 +438,7 @@ export default function TaskDetailPage() {
setReplyDrafts((prev) => ({ ...prev, [parentId]: "" }))
setOpenReplyEditors((prev) => ({ ...prev, [parentId]: false }))
const success = await updateTask(nextTask.id, {
...nextTask,
comments: getComments(nextTask.comments),
})
const success = await updateTask(nextTask.id, nextTask)
if (!success) {
toast.error("Failed to save reply", {
description: "Reply was added locally but could not sync to the server.",
@ -459,7 +452,7 @@ export default function TaskDetailPage() {
setEditedTask({
...editedTask,
comments: removeCommentFromThread(getComments(editedTask.comments), commentId),
comments: removeCommentFromThread(editedTask.comments, commentId),
})
}
@ -535,10 +528,7 @@ export default function TaskDetailPage() {
setSaveSuccess(false)
try {
const success = await updateTask(editedTask.id, {
...editedTask,
comments: getComments(editedTask.comments),
})
const success = await updateTask(editedTask.id, editedTask)
if (success) {
setSaveSuccess(true)
@ -614,7 +604,7 @@ export default function TaskDetailPage() {
const renderThread = (comments: TaskComment[], depth = 0) =>
comments.map((comment) => {
const replies = getComments(comment.replies)
const replies = comment.replies || []
const isReplying = !!openReplyEditors[comment.id]
const replyDraft = replyDrafts[comment.id] || ""
const authorId = comment.commentAuthorId
@ -1033,14 +1023,14 @@ export default function TaskDetailPage() {
<div className="border-t border-slate-800 pt-6">
<h4 className="font-medium text-white mb-4 flex items-center gap-2">
<MessageSquare className="w-4 h-4" />
Comments ({commentCount})
Comments ({editedTask.comments.length})
</h4>
<div className="space-y-3 mb-4">
{commentCount === 0 ? (
{editedTask.comments.length === 0 ? (
<p className="text-slate-500 text-sm">No comments yet. Add the first one.</p>
) : (
renderThread(getComments(editedTask.comments))
renderThread(editedTask.comments)
)}
</div>