Skill: yayayahei/eisenhower-task-manager (version 1.1.1)

--- name: task-manager description: Task management based on Eisenhower Matrix + P0-P2 priority with Customer Project Management. Four quadrants for execution, separate Customer Project List for all customer work. --- # Task Manager - Four-Level System ## Architecture **Tasks Directory Configuration:** By default, the skill operates in the `tasks/` directory relative to this SKILL.md. You can override this globally by setting the `EISENHOWER_TASKS_DIR` environment variable to an absolute path (e.g., `export EISENHOWER_TASKS_DIR=/Users/xxx/tasks`). | Layer | File | Purpose | |-------|------|---------| | Customer | `tasks/customer-projects.md` | ALL customer projects (single source of truth) | | Execution | `tasks/tasks.md` | Four quadrants (personal + customer tasks requiring YOUR execution) | | Future | `tasks/maybe.md` | Ideas for later evaluation | | Delegation | `tasks/delegation.md` | Tasks assigned to others | | Archive | `tasks/archived.md` | Completed/deleted history | ## Core Principles 1. **Customer projects NEVER duplicate** - exist only in Customer Project List 2. **Personal execution only** - customer tasks enter quadrants only when YOU must execute 3. **Sequential numbering** - 1-N per file, no gaps, no duplicates 4. **Auto-fix numbering** - after EVERY operation, verify and fix sequence ## Four Quadrants | Q | Name | Strategy | When | |---|------|----------|------| | Q1 | Important + Urgent | Execute immediately | Deadlines, blockers | | Q2 | Important + Not Urgent | Plan & schedule | OKRs, strategic work | | Q3 | Not Important + Urgent | Batch process | Maintenance, delegatable | | Q4 | Not Important + Not Urgent | Postpone/delete | Exploration, optional | ## Priority Tiers (within quadrant) - **P0**: Today (hard deadline or blocking) - **P1**: This week - **P2**: This month/quarter ## Customer Project Workflow ``` New Customer Project ↓ Add to Customer Project List (record all) ↓ Need YOUR execution? ├─ YES → Add to appropriate Quadrant, tag with [Customer/ProjectName] └─ NO → Keep in Customer Project List only ↓ Regular Review → Update status (Active/Blocked/Pending) ↓ Completed → Archive entry + complete quadrant task if exists ``` ## Task Numbering Rules ### Quadrant Tasks (tasks.md) - Q1: 1 to N - Q2: (N+1) to M - Q3: (M+1) to P - Q4: (P+1) to Q - Continuous sequence across quadrants, no gaps ### Maybe List (maybe.md) - Sequential: 1, 2, 3... - **ALWAYS append to END** - never insert in middle - Verify: last_number == total_count ### Customer Projects (customer-projects.md) - Per-customer numbering: 1, 2, 3... - Independent across customers ### Delegation List (delegation.md) - Sequential: 1, 2, 3... - **ALWAYS append to END** - Verify: last_number == total_count - Format: `### X. Task Name @assignee` ### Archive (archived.md) - Sequential: 1, 2, 3... - **ALWAYS append to END** - Group by archive date - Format: `### X. Task Name [Archived: YYYY-MM-DD]` ## Numbering Verification (MUST RUN AFTER EVERY OPERATION) 1. Read target file 2. Scan for: duplicates, gaps, wrong transitions 3. Reassign sequential numbers starting from 1 4. Re-read to verify 5. Repeat if issues remain **Numbering Checklist:** - [ ] Sequential from 1, no gaps - [ ] No duplicates - [ ] Q2 starts at Q1_count+1, Q3 at Q2_end+1, etc. ## Operations ### Add Task See [references/task-add.md](references/task-add.md) for complete workflow. **Quick Guide:** 1. Determine level: Quadrants / Delegation / Maybe 2. Determine quadrant (if applicable): Q1/Q2/Q3/Q4 3. Assign priority: P0/P1/P2 4. Insert at appropriate position 5. Renumber ALL tasks (sequential from 1) 6. Update statistics 7. Display updated list 8. **(Optional)** Offer to open Dashboard for visual review ### Complete Task See [references/task-complete.md](references/task-complete.md) for complete workflow. **Key Points:** - **MUST NOT stop at just marking [x]** - full flow required - Delete completed task from source file - Renumber remaining tasks - Move to archived.md - Update statistics - Display updated list - **(Optional)** Offer to open Dashboard to see updated status **Three Scenarios:** 1. **Main Task List** (tasks.md) - Four Quadrant tasks 2. **Delegation List** (delegation.md) - Delegated tasks 3. **Maybe List** (maybe.md) - Future tasks ### Post-Operation Dashboard Offer After any task operation (add/complete/update), the skill SHOULD offer to open the dashboard: ```markdown ✅ Task Added Successfully **Summary:** - Task: [Task Name] - Location: Q2 #15 - Priority: P1 📊 Would you like to view the task dashboard? → User approves: Launch dashboard and open browser → User declines: Continue with next operation ``` **Implementation:** Check if user wants to open dashboard after major operations. ### Add Customer Project **Step 1**: Add to Customer Project List ```markdown ### [Customer Name] #### X. Project Name [Status] - **Status**: Active / Blocked / Pending / Completed - **Type**: Implementation / Support / POC / Maintenance - **Description**: Brief - **Created**: YYYY-MM-DD - **Last Review**: YYYY-MM-DD - **Notes**: Progress, blockers ``` **Step 2**: If personal execution needed, also add to Quadrants ### Complete Customer Project 1. Mark Completed in Customer Project List 2. Archive the entry in archived.md 3. Complete related quadrant task if exists ### Add to Maybe List 1. Read `tasks/maybe.md` 2. Find highest task number (N) 3. Append new task as #(N+1) at END 4. Update total count and timestamp ### Check/Fix Numbering Trigger phrases: "check task numbering", "fix sequence", "序号问题" **Action**: Read file → scan issues → auto-fix → re-verify → repeat if needed ## Output Format (MUST OUTPUT AFTER EVERY OPERATION) ```markdown ✅ [Operation] Completed **Summary:** - Operation: [Add/Update/Complete/Move/Delete] - Target: [Task name/ID] - Location: [Q1/Q2/Q3/Q4/Customer/Maybe/Delegation/Archive] **Statistics:** | Quadrant | Count | |----------|-------| | Q1 | X | | Q2 | X | | Q3 | X | | Q4 | X | ``` See [references/output-examples.md](references/output-examples.md) for detailed examples. ## Healthy Metrics | List | Target | Warning | |------|--------|---------| | Customer Projects | 5-15 | - | | Q1 | 3-5 | >5 indicates poor planning | | Q2 | 8-12 | Core investment area | | Q3 | 5-10 | Automate gradually | | Q4 | <10 | Clean up regularly | | Maybe | <20 | Review monthly | | Delegation | <15 | Review weekly | ## Time Rhythm - **Daily**: Check Q1, customer escalations - **Weekly**: Review Customer Projects, Q2 planning, Delegation check - **Monthly**: Archive completed, review blocked, adjust priorities ## Task Tags - Customer tasks in quadrants: `[Customer/ProjectName]` - Blocked tasks: `🚫 Blocked` - Status: P0/P1/P2 ## Dashboard (Optional) A real-time web dashboard is available to view all tasks in a friendly format. **Location**: `dashboard/` **Features**: - 📊 Real-time statistics showing task counts by quadrant - 🔥 Four-quadrant matrix view with color coding - 🏢 Customer projects overview with filtering - 👑 Delegation list with assignee and deadline info - 🌱 Maybe list for future ideas - 🎨 Dark theme with modern UI design - 🌐 Bilingual support (English/Chinese) - ⚡ Real-time updates via WebSocket when markdown files change - 📱 Responsive layout for different screen sizes - 💡 **Hover to see full task details** - Mouse over any task card to see complete information including description, subtasks, tags, and timestamps ### Dashboard Launch Workflow After completing task operations, the skill will **offer** to open the dashboard: ``` ✅ [Operation] Completed Would you like to view the task dashboard? [Open Dashboard] [Skip] If approved → Auto-start dashboard and open browser If skipped → Dashboard remains available for manual launch ``` ### Manual Launch (Anytime) #### Method 1: Daemon Mode (Recommended for Long-term Use) Run in background, service continues after terminal closes: ```bash cd dashboard # Start in background, no terminal dependency ./start.sh --daemon # Optional: You can specify custom task directory via flag or environment variable ./start.sh --daemon --tasks-dir /your/custom/path # OR export EISENHOWER_TASKS_DIR=/your/custom/path ./start.sh --daemon ``` **Features**: - ✅ Service continues after terminal closes - ✅ Auto PID management, prevents duplicate starts - ✅ Logs output to `dashboard.log` - ✅ Shows access URL and PID after startup Stop the daemon: ```bash ./stop.sh # Gracefully stop the service ``` #### Method 2: Foreground Mode (For Debugging) Suitable for development and debugging, service stops when terminal closes: ```bash cd dashboard ./start.sh # Run in foreground, Ctrl+C to stop ``` #### Advanced Usage ```bash # Specify port (default 8080) ./start.sh --daemon --port 3000 # Check running status curl http://localhost:8080/api/health ``` ### Architecture **Tasks Directory Configuration:** By default, the skill operates in the `tasks/` directory relative to this SKILL.md. You can override this globally by setting the `EISENHOWER_TASKS_DIR` environment variable to an absolute path (e.g., `export EISENHOWER_TASKS_DIR=/Users/xxx/tasks`). - Markdown files are the single source of truth - Node.js server parses markdown in real-time - WebSocket pushes updates to browser when files change - No database required - pure markdown-driven - Enhanced error handling: global exception capture, port conflict detection ## Reference Materials | File | Purpose | |------|---------| | `references/task-add.md` | **Complete workflow for adding tasks** - levels, quadrants, priorities, renumbering | | `references/task-complete.md` | **Complete workflow for completing tasks** - 3 scenarios (Main/Delegation/Maybe), archiving | | `references/numbering-rules.md` | Numbering rules for all 5 task lists | | `references/output-examples.md` | Output format examples | | `references/maybe-list-workflow.md` | Step-by-step Maybe List operations | | `references/templates.md` | Statistics and report templates | | `references/dashboard-offer.md` | Dashboard offer workflow after task operations | | `dashboard/` | Real-time web dashboard (`./start.sh --daemon` for background mode) | **Critical**: Always read `task-add.md` or `task-complete.md` before performing those operations to ensure full workflow compliance. --- *Version: 8.2 (Added daemon mode, stop.sh, enhanced error handling)* *Last Updated: 2026-04-21*

path: dashboard/README.md
content: # Eisenhower Task Dashboard

Dynamic task dashboard for the eisenhower-task-manager skill with real-time Web interface.

## Features

- 📊 **Real-time Statistics** - Task counts by quadrant, customers, delegation, and maybe list
- 🔥 **Four-Quadrant Matrix** - Visual Q1-Q4 task distribution
- 🏢 **Customer Projects** - View all customers and their project status
- 👑 **Delegation** - Track tasks assigned to others
- 🌱 **Maybe List** - Manage "might do later" ideas
- ⚡ **Real-time Updates** - WebSocket pushes file changes instantly
- 🎨 **Dark Theme** - Modern dark UI design
- 🌐 **Bilingual** - English/Chinese language support

## Quick Start

### Auto-install & Run (Recommended)

```bash
cd dashboard
./start.sh              # Auto-installs deps on first run, then starts server
```

The script will:
1. Check if `node_modules` exists (install if missing)
2. Read your saved port (or use default 8080)
3. Start the server
4. Open `http://localhost:8080` in your browser

### Manual Setup

```bash
cd dashboard
npm install             # One-time setup
./start.sh              # Start server

# Or use custom port
./start.sh --port 3000
```

## Scripts

| Script | Description |
|--------|-------------|
| `./start.sh` | Start server with auto-install and port memory |
| `./check-and-install.sh` | Check and install dependencies only |
| `npm start` | Start server directly (requires deps installed) |

## API Endpoints

- `GET /api/health` - Health check
- `GET /api/tasks` - Get all task data
- `WS /` - WebSocket real-time updates

## File Structure

```
dashboard/
├── server.js                # Express + WebSocket server
├── parser.js                # Markdown parser
├── package.json             # Dependencies
├── start.sh                 # Auto-install & start script ⭐
├── check-and-install.sh     # Dependency checker ⭐
├── port.conf                # Saved port config (auto-generated)
├── .gitignore               # Excludes node_modules
└── public/                  # Frontend files
    ├── index.html
    ├── style.css
    ├── app.js
    ├── i18n.js              # Bilingual support
    └── favicon.svg          # App icon
```

## Tech Stack

- **Backend**: Node.js, Express, WebSocket (ws), chokidar
- **Frontend**: Vanilla JavaScript, CSS3, HTML5
- **Real-time**: WebSocket

## Data Source

Automatically reads from `../tasks/` directory:
- `tasks.md` - Four-quadrant tasks
- `customer-projects.md` - Customer projects
- `delegation.md` - Delegated tasks
- `maybe.md` - Future ideas

## Port Configuration

The dashboard remembers your last used port:

```bash
# First run with custom port
./start.sh --port 3000

# Subsequent runs will use port 3000 automatically
./start.sh
```

Default port: `8080`


path: dashboard/check-and-install.sh
content: #!/bin/bash
# Check and install dashboard dependencies if needed

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

cd "$SCRIPT_DIR"

# Check if node_modules exists
if [ ! -d "node_modules" ]; then
  echo "[Dashboard] Dependencies not found. Installing..."
  
  # Check if npm is available
  if ! command -v npm &> /dev/null; then
    echo "[Error] npm not found. Please install Node.js first."
    echo "Visit: https://nodejs.org/"
    exit 1
  fi
  
  # Install dependencies
  npm install
  
  if [ $? -eq 0 ]; then
    echo "[Dashboard] Dependencies installed successfully!"
  else
    echo "[Error] Failed to install dependencies."
    exit 1
  fi
else
  echo "[Dashboard] Dependencies already installed."
fi


path: dashboard/package-lock.json
content: {
  "name": "eisenhower-task-dashboard",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "eisenhower-task-dashboard",
      "version": "1.0.0",
      "dependencies": {
        "chokidar": "^3.5.3",
        "express": "^4.18.2",
        "ws": "^8.14.2"
      }
    },
    "node_modules/accepts": {
      "version": "1.3.8",
      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-types": "~2.1.34",
        "negotiator": "0.6.3"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/anymatch": {
      "version": "3.1.3",
      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "normalize-path": "^3.0.0",
        "picomatch": "^2.0.4"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/array-flatten": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/binary-extensions": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/body-parser": {
      "version": "1.20.5",
      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "bytes": "~3.1.2",
        "content-type": "~1.0.5",
        "debug": "2.6.9",
        "depd": "2.0.0",
        "destroy": "~1.2.0",
        "http-errors": "~2.0.1",
        "iconv-lite": "~0.4.24",
        "on-finished": "~2.4.1",
        "qs": "~6.15.1",
        "raw-body": "~2.5.3",
        "type-is": "~1.6.18",
        "unpipe": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8",
        "npm": "1.2.8000 || >= 1.4.16"
      }
    },
    "node_modules/braces": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "fill-range": "^7.1.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/bytes": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/call-bind-apply-helpers": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/call-bound": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "get-intrinsic": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/chokidar": {
      "version": "3.6.0",
      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "anymatch": "~3.1.2",
        "braces": "~3.0.2",
        "glob-parent": "~5.1.2",
        "is-binary-path": "~2.1.0",
        "is-glob": "~4.0.1",
        "normalize-path": "~3.0.0",
        "readdirp": "~3.6.0"
      },
      "engines": {
        "node": ">= 8.10.0"
      },
      "funding": {
        "url": "https://paulmillr.com/funding/"
      },
      "optionalDependencies": {
        "fsevents": "~2.3.2"
      }
    },
    "node_modules/content-disposition": {
      "version": "0.5.4",
      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safe-buffer": "5.2.1"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/content-type": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/cookie": {
      "version": "0.7.2",
      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/cookie-signature": {
      "version": "1.0.7",
      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/debug": {
      "version": "2.6.9",
      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ms": "2.0.0"
      }
    },
    "node_modules/depd": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/destroy": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8",
        "npm": "1.2.8000 || >= 1.4.16"
      }
    },
    "node_modules/dunder-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.1",
        "es-errors": "^1.3.0",
        "gopd": "^1.2.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/ee-first": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/encodeurl": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/es-define-property": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-errors": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-object-atoms": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/escape-html": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/etag": {
      "version": "1.8.1",
      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/express": {
      "version": "4.22.2",
      "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "accepts": "~1.3.8",
        "array-flatten": "1.1.1",
        "body-parser": "~1.20.5",
        "content-disposition": "~0.5.4",
        "content-type": "~1.0.4",
        "cookie": "~0.7.1",
        "cookie-signature": "~1.0.6",
        "debug": "2.6.9",
        "depd": "2.0.0",
        "encodeurl": "~2.0.0",
        "escape-html": "~1.0.3",
        "etag": "~1.8.1",
        "finalhandler": "~1.3.1",
        "fresh": "~0.5.2",
        "http-errors": "~2.0.0",
        "merge-descriptors": "1.0.3",
        "methods": "~1.1.2",
        "on-finished": "~2.4.1",
        "parseurl": "~1.3.3",
        "path-to-regexp": "~0.1.12",
        "proxy-addr": "~2.0.7",
        "qs": "~6.15.1",
        "range-parser": "~1.2.1",
        "safe-buffer": "5.2.1",
        "send": "~0.19.0",
        "serve-static": "~1.16.2",
        "setprototypeof": "1.2.0",
        "statuses": "~2.0.1",
        "type-is": "~1.6.18",
        "utils-merge": "1.0.1",
        "vary": "~1.1.2"
      },
      "engines": {
        "node": ">= 0.10.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/fill-range": {
      "version": "7.1.1",
      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "to-regex-range": "^5.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/finalhandler": {
      "version": "1.3.2",
      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "2.6.9",
        "encodeurl": "~2.0.0",
        "escape-html": "~1.0.3",
        "on-finished": "~2.4.1",
        "parseurl": "~1.3.3",
        "statuses": "~2.0.2",
        "unpipe": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/forwarded": {
      "version": "0.2.0",
      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/fresh": {
      "version": "0.5.2",
      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/fsevents": {
      "version": "2.3.3",
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "hasInstallScript": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
      }
    },
    "node_modules/function-bind": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-intrinsic": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "es-define-property": "^1.0.1",
        "es-errors": "^1.3.0",
        "es-object-atoms": "^1.1.1",
        "function-bind": "^1.1.2",
        "get-proto": "^1.0.1",
        "gopd": "^1.2.0",
        "has-symbols": "^1.1.0",
        "hasown": "^2.0.2",
        "math-intrinsics": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "dunder-proto": "^1.0.1",
        "es-object-atoms": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/glob-parent": {
      "version": "5.1.2",
      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "is-glob": "^4.0.1"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/gopd": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-symbols": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/hasown": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/http-errors": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "depd": "~2.0.0",
        "inherits": "~2.0.4",
        "setprototypeof": "~1.2.0",
        "statuses": "~2.0.2",
        "toidentifier": "~1.0.1"
      },
      "engines": {
        "node": ">= 0.8"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/iconv-lite": {
      "version": "0.4.24",
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safer-buffer": ">= 2.1.2 < 3"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/ipaddr.js": {
      "version": "1.9.1",
      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/is-binary-path": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "binary-extensions": "^2.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/is-extglob": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/is-glob": {
      "version": "4.0.3",
      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "is-extglob": "^2.1.1"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/is-number": {
      "version": "7.0.0",
      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.12.0"
      }
    },
    "node_modules/math-intrinsics": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/media-typer": {
      "version": "0.3.0",
      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/merge-descriptors": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/methods": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime": {
      "version": "1.6.0",
      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "bin": {
        "mime": "cli.js"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/mime-db": {
      "version": "1.52.0",
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime-types": {
      "version": "2.1.35",
      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-db": "1.52.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/ms": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/negotiator": {
      "version": "0.6.3",
      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/normalize-path": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/object-inspect": {
      "version": "1.13.4",
      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/on-finished": {
      "version": "2.4.1",
      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ee-first": "1.1.1"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/parseurl": {
      "version": "1.3.3",
      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/path-to-regexp": {
      "version": "0.1.13",
      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/picomatch": {
      "version": "2.3.2",
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8.6"
      },
      "funding": {
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
    "node_modules/proxy-addr": {
      "version": "2.0.7",
      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "forwarded": "0.2.0",
        "ipaddr.js": "1.9.1"
      },
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/qs": {
      "version": "6.15.2",
      "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause",
      "dependencies": {
        "side-channel": "^1.1.0"
      },
      "engines": {
        "node": ">=0.6"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/range-parser": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/raw-body": {
      "version": "2.5.3",
      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "bytes": "~3.1.2",
        "http-errors": "~2.0.1",
        "iconv-lite": "~0.4.24",
        "unpipe": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/readdirp": {
      "version": "3.6.0",
      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "picomatch": "^2.2.1"
      },
      "engines": {
        "node": ">=8.10.0"
      }
    },
    "node_modules/safe-buffer": {
      "version": "5.2.1",
      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT"
    },
    "node_modules/safer-buffer": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/send": {
      "version": "0.19.2",
      "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "2.6.9",
        "depd": "2.0.0",
        "destroy": "1.2.0",
        "encodeurl": "~2.0.0",
        "escape-html": "~1.0.3",
        "etag": "~1.8.1",
        "fresh": "~0.5.2",
        "http-errors": "~2.0.1",
        "mime": "1.6.0",
        "ms": "2.1.3",
        "on-finished": "~2.4.1",
        "range-parser": "~1.2.1",
        "statuses": "~2.0.2"
      },
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/send/node_modules/ms": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/serve-static": {
      "version": "1.16.3",
      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "encodeurl": "~2.0.0",
        "escape-html": "~1.0.3",
        "parseurl": "~1.3.3",
        "send": "~0.19.1"
      },
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/setprototypeof": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/side-channel": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3",
        "side-channel-list": "^1.0.0",
        "side-channel-map": "^1.0.1",
        "side-channel-weakmap": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-list": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.4"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-map": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-weakmap": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3",
        "side-channel-map": "^1.0.1"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/statuses": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/to-regex-range": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "is-number": "^7.0.0"
      },
      "engines": {
        "node": ">=8.0"
      }
    },
    "node_modules/toidentifier": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.6"
      }
    },
    "node_modules/type-is": {
      "version": "1.6.18",
      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "media-typer": "0.3.0",
        "mime-types": "~2.1.24"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/unpipe": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/utils-merge": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4.0"
      }
    },
    "node_modules/vary": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/ws": {
      "version": "8.20.1",
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=10.0.0"
      },
      "peerDependencies": {
        "bufferutil": "^4.0.1",
        "utf-8-validate": ">=5.0.2"
      },
      "peerDependenciesMeta": {
        "bufferutil": {
          "optional": true
        },
        "utf-8-validate": {
          "optional": true
        }
      }
    }
  }
}


path: dashboard/package.json
content: {
  "name": "eisenhower-task-dashboard",
  "version": "1.0.0",
  "description": "Dynamic task dashboard for Eisenhower Task Manager",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "node server.js --port 8080"
  },
  "dependencies": {
    "express": "^4.18.2",
    "ws": "^8.14.2",
    "chokidar": "^3.5.3"
  }
}


path: dashboard/parser.js
content: /**
 * Markdown Parser for Eisenhower Task Manager
 * Parses tasks.md, customer-projects.md, delegation.md, maybe.md
 */

const fs = require('fs');
const path = require('path');

// Tasks are located in workspace/tasks/ directory (4 levels up from dashboard)
const EISENHOWER_TASKS_DIR = process.env.EISENHOWER_TASKS_DIR || path.join(__dirname, '../tasks');

/**
 * Parse tasks.md - Four Quadrants Format
 */
function parseTasks(content) {
  const result = {
    q1: [],
    q2: [],
    q3: [],
    q4: [],
    stats: { q1: 0, q2: 0, q3: 0, q4: 0, total: 0 }
  };

  // Stats will be calculated based on actual parsed tasks (more accurate than header text)

  // Split by quadrants - support both half-width () and full-width () parentheses
  const q1Match = content.match(/## 🔥 Q1[\s\S]*?(?=## 💼 Q2|$)/);
  const q2Match = content.match(/## 💼 Q2[\s\S]*?(?=## ⚡ Q3|$)/);
  const q3Match = content.match(/## ⚡ Q3[\s\S]*?(?=## 🧘 Q4|$)/);
  const q4Match = content.match(/## 🧘 Q4[\s\S]*?(?=## 👑|$)/);

  if (q1Match) result.q1 = parseQuadrantTasks(q1Match[0], 'Q1');
  if (q2Match) result.q2 = parseQuadrantTasks(q2Match[0], 'Q2');
  if (q3Match) result.q3 = parseQuadrantTasks(q3Match[0], 'Q3');
  if (q4Match) result.q4 = parseQuadrantTasks(q4Match[0], 'Q4');

  // Calculate stats based on actual parsed tasks (more accurate than header text)
  result.stats.q1 = result.q1.length;
  result.stats.q2 = result.q2.length;
  result.stats.q3 = result.q3.length;
  result.stats.q4 = result.q4.length;
  result.stats.total = result.q1.length + result.q2.length + result.q3.length + result.q4.length;

  return result;
}

function parseQuadrantTasks(content, quadrant) {
  const tasks = [];
  // Match task sections: ### X. Task Name [tags]
  const taskRegex = /### (\d+)\.\s*(.+?)(?:\s*\[([^\]]+)\])?\n([\s\S]*?)(?=### \d+\.|## |\n## |\n---|$)/g;

  let match;
  while ((match = taskRegex.exec(content)) !== null) {
    const taskContent = match[4];
    const task = {
      id: parseInt(match[1]),
      title: match[2].trim(),
      tags: match[3] ? match[3].split('/').map(t => t.trim()) : [],
      quadrant: quadrant,
      status: extractStatus(taskContent),
      priority: extractPriority(taskContent),
      description: extractDescription(taskContent),
      created: extractDate(taskContent, '创建'),
      updated: extractDate(taskContent, '更新'),
      blocked: taskContent.includes('🚫') || taskContent.includes('阻塞'),
      subtasks: extractSubtasks(taskContent),
      raw: match[0]
    };
    tasks.push(task);
  }

  return tasks;
}

function extractStatus(content) {
  const statusMatch = content.match(/\*\*状态\*\*:(.+)/);
  if (statusMatch) {
    const status = statusMatch[1].trim();
    if (status.includes('P0')) return 'P0';
    if (status.includes('P1')) return 'P1';
    if (status.includes('P2')) return 'P2';
    return status;
  }
  return '';
}

function extractPriority(content) {
  if (content.includes('P0')) return 'P0';
  if (content.includes('P1')) return 'P1';
  if (content.includes('P2')) return 'P2';
  return '';
}

function extractDescription(content) {
  const descMatch = content.match(/\*\*描述\*\*:(.+)/);
  return descMatch ? descMatch[1].trim() : '';
}

function extractDate(content, type) {
  const dateMatch = content.match(new RegExp(`\\*\\*${type}\\*\\*:(.+)`));
  return dateMatch ? dateMatch[1].trim() : '';
}

function extractSubtasks(content) {
  const subtasks = [];
  const subtaskRegex = /-\s*\*\*(.+?)\*\*\n([\s\S]*?)(?=\n\s*-\s*\*\*|\n### |\n## |$)/g;

  let match;
  while ((match = subtaskRegex.exec(content)) !== null) {
    subtasks.push({
      title: match[1].trim(),
      content: match[2].trim()
    });
  }

  return subtasks;
}

/**
 * Parse customer-projects.md - Customer Project List Format
 */
function parseCustomerProjects(content) {
  const result = {
    customers: [],
    stats: { active: 0, blocked: 0, pending: 0, total: 0 }
  };

  // Extract stats from overview table (support both Chinese and English)
  const activeMatch = content.match(/🟢\s*(?:Active|进行中)\s*\|\s*(\d+)/);
  const blockedMatch = content.match(/🟡\s*(?:Blocked|阻塞中|阻塞)\s*\|\s*(\d+)/);
  const pendingMatch = content.match(/🔵\s*(?:Pending|待开始)\s*\|\s*(\d+)/);
  const totalMatch = content.match(/\*\*\s*(?:Total|总计)\s*\*\*\s*\|\s*\*\*(\d+)\*\*/);

  if (activeMatch) result.stats.active = parseInt(activeMatch[1]);
  if (blockedMatch) result.stats.blocked = parseInt(blockedMatch[1]);
  if (pendingMatch) result.stats.pending = parseInt(pendingMatch[1]);
  if (totalMatch) result.stats.total = parseInt(totalMatch[1]);

  // Parse customers and their projects
  // Support format: ### 🔴 优先级1：Name (Chinese colon)
  // Note: Emoji is 2 UTF-16 code units (surrogate pair)
  // Use split parsing: first find customer headers, then extract sections manually
  const customerHeaders = Array.from(content.matchAll(/^###\s+(.{1,2})\s+优先级[\d一二三四五]+[：:](.+)$/gmu));

  for (let i = 0; i < customerHeaders.length; i++) {
    const headerMatch = customerHeaders[i];
    const startPos = headerMatch.index + headerMatch[0].length;
    const endPos = i < customerHeaders.length - 1 ? customerHeaders[i + 1].index : content.length;
    const section = content.substring(startPos, endPos);

    const customer = {
      name: headerMatch[2].trim(),
      priority: headerMatch[1].trim(),
      projects: []
    };

    // Parse projects within customer section
    // Match #### (project level)
    const projectRegex = /^#### (\d+)\.\s*(.+?)\n/gm;
    let projMatch;
    while ((projMatch = projectRegex.exec(section)) !== null) {
      const projStart = projMatch.index + projMatch[0].length;
      // Find where this project ends (next #### or end of section)
      const nextProjMatch = /^#### \d+\./m.exec(section.substring(projStart));
      const projEnd = nextProjMatch ? projStart + nextProjMatch.index : section.length;
      const projContent = section.substring(projStart, projEnd);

      const project = {
        id: parseInt(projMatch[1]),
        name: projMatch[2].trim(),
        status: extractField(projContent, '状态') || extractField(projContent, 'Status'),
        type: extractField(projContent, '类型') || extractField(projContent, 'Type'),
        priority: extractField(projContent, '优先级') || extractField(projContent, 'Priority'),
        created: extractField(projContent, '创建时间') || extractField(projContent, 'Created'),
        lastReview: extractField(projContent, '上次回顾') || extractField(projContent, 'Last Review'),
        nextReview: extractField(projContent, '下次检查') || extractField(projContent, 'Next Review'),
        notes: extractField(projContent, '备注') || extractField(projContent, 'Notes'),
        quadrantTask: extractField(projContent, '象限任务') || extractField(projContent, 'Quadrant Task'),
        blocked: projContent.includes('🟡') || projContent.includes('阻塞'),
        raw: projMatch[0] + projContent  // Save original content for regeneration
      };
      customer.projects.push(project);
    }

    result.customers.push(customer);
  }

  return result;
}

function extractField(content, fieldName) {
  // Escape special regex characters in fieldName
  const escapedFieldName = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  // Support Chinese colon (:) - the document uses Chinese colons
  // Support optional markdown checkbox: - [ ], - [x], or just - 
  const regex = new RegExp(`-(?: \\[[ xX]\\])? \\*\\*${escapedFieldName}\\*\\*:(.+)`, 'im');
  const match = content.match(regex);
  return match ? match[1].trim() : '';
}

/**
 * Parse delegation.md - Delegation List Format
 */
function parseDelegation(content) {
  const result = {
    tasks: [],
    stats: { total: 0, inProgress: 0, delegated: 0, overdue: 0 }
  };

  // Extract total count
  const totalMatch = content.match(/总任务数:(\d+) 个/);
  if (totalMatch) result.stats.total = parseInt(totalMatch[1]);

  // Parse delegation tasks
  const taskRegex = /#### (\d+)\.\s*(.+?)\n([\s\S]*?)(?=#### \d+\.|## |\n---|$)/g;

  let match;
  while ((match = taskRegex.exec(content)) !== null) {
    const taskContent = match[3];
    const task = {
      id: parseInt(match[1]),
      title: match[2].trim(),
      status: extractField(taskContent, '状态'),
      assignee: extractAssignee(taskContent),
      description: extractField(taskContent, '说明'),
      deadline: extractField(taskContent, 'Deadline'),
      created: extractField(taskContent, '创建时间'),
      lastReview: extractField(taskContent, 'Last Review'),
      nextReview: extractField(taskContent, 'Next Review'),
      overdue: taskContent.includes('⚠️') || taskContent.includes('已过期'),
      raw: match[0]
    };
    result.tasks.push(task);
  }

  // Calculate stats based on actual parsed tasks
  result.stats.total = result.tasks.length;
  result.stats.inProgress = result.tasks.filter(t => t.status === '进行中').length;
  result.stats.delegated = result.tasks.filter(t => t.status === '待开始').length;
  result.stats.overdue = result.tasks.filter(t => t.overdue).length;

  return result;
}

function extractAssignee(content) {
  const match = content.match(/\*\*责任人\*\*:(.+)/);
  return match ? match[1].trim() : '';
}

/**
 * Parse maybe.md - Maybe List Format
 */
function parseMaybeList(content) {
  const result = {
    tasks: [],
    stats: { total: 0 }
  };

  // Parse maybe tasks
  const taskRegex = /#### (\d+)\.\s*(.+?)\n([\s\S]*?)(?=#### \d+\.|## |\n---|$)/g;

  let match;
  while ((match = taskRegex.exec(content)) !== null) {
    const taskContent = match[3];
    const rawTitle = match[2].trim();

    // Extract category from title (e.g., "任务名【分类】" -> category: "分类")
    const categoryMatch = rawTitle.match(/【(.+?)】$/);
    const category = categoryMatch ? categoryMatch[1] : '';
    // Remove category marker from title
    const title = rawTitle.replace(/【.+?】$/, '').trim();

    const task = {
      id: parseInt(match[1]),
      title: title,
      status: extractField(taskContent, '状态'),
      description: extractField(taskContent, '说明'),
      content: taskContent.trim(),
      created: extractField(taskContent, '创建时间'),
      category: category,
      raw: match[0]
    };
    result.tasks.push(task);
  }

  // Calculate stats based on actual parsed tasks
  result.stats.total = result.tasks.length;

  return result;
}

function extractCategory(content) {
  const match = content.match(/【(.+?)】/);
  return match ? match[1] : '';
}

/**
 * Load and parse all task files
 */
function loadAllTasks() {
  const data = {
    timestamp: new Date().toISOString()
  };

  try {
    const tasksContent = fs.readFileSync(path.join(EISENHOWER_TASKS_DIR, 'tasks.md'), 'utf8');
    data.tasks = parseTasks(tasksContent);
  } catch (e) {
    data.tasks = { error: e.message, q1: [], q2: [], q3: [], q4: [], stats: {} };
  }

  try {
    const customerContent = fs.readFileSync(path.join(EISENHOWER_TASKS_DIR, 'customer-projects.md'), 'utf8');
    data.customerProjects = parseCustomerProjects(customerContent);
  } catch (e) {
    data.customerProjects = { error: e.message, customers: [], stats: {} };
  }

  try {
    const delegationContent = fs.readFileSync(path.join(EISENHOWER_TASKS_DIR, 'delegation.md'), 'utf8');
    data.delegation = parseDelegation(delegationContent);
  } catch (e) {
    data.delegation = { error: e.message, tasks: [], stats: {} };
  }

  try {
    const maybeContent = fs.readFileSync(path.join(EISENHOWER_TASKS_DIR, 'maybe.md'), 'utf8');
    data.maybe = parseMaybeList(maybeContent);
  } catch (e) {
    data.maybe = { error: e.message, tasks: [], stats: {} };
  }

  return data;
}

/**
 * Move a task between quadrants or reorder within the same quadrant
 * @param {number} taskId - The task ID to move
 * @param {string} sourceQuadrant - Source quadrant (Q1, Q2, Q3, Q4)
 * @param {string} targetQuadrant - Target quadrant (Q1, Q2, Q3, Q4)
 * @param {number} insertIndex - Position to insert in target quadrant (-1 for append)
 */
async function moveTask(taskId, sourceQuadrant, targetQuadrant, insertIndex = -1) {
  try {
    console.log(`[Parser] Moving task ${taskId} from ${sourceQuadrant} to ${targetQuadrant} at index ${insertIndex}`);

    const tasksFile = path.join(EISENHOWER_TASKS_DIR, 'tasks.md');
    const content = fs.readFileSync(tasksFile, 'utf8');

    // Parse current tasks
    const tasks = parseTasks(content);

    // Find the task to move
    const sourceList = tasks[sourceQuadrant.toLowerCase()];
    const targetList = tasks[targetQuadrant.toLowerCase()];

    if (!sourceList || !targetList) {
      return { success: false, error: `Invalid quadrant: ${sourceQuadrant} or ${targetQuadrant}` };
    }

    const taskIndex = sourceList.findIndex(t => t.id === taskId);
    if (taskIndex === -1) {
      return { success: false, error: `Task ${taskId} not found in ${sourceQuadrant}` };
    }

    const taskToMove = sourceList[taskIndex];

    // Update task quadrant
    taskToMove.quadrant = targetQuadrant;

    // Remove from source
    sourceList.splice(taskIndex, 1);

    // Insert into target
    if (insertIndex === -1 || insertIndex >= targetList.length) {
      targetList.push(taskToMove);
    } else {
      targetList.splice(insertIndex, 0, taskToMove);
    }

    // Generate new markdown content (this will handle renumbering)
    const newContent = generateTasksMarkdown(tasks, content);

    // Write back to file
    fs.writeFileSync(tasksFile, newContent, 'utf8');

    // Calculate the new ID for the moved task
    let newId;
    if (targetQuadrant === 'Q1') {
      newId = insertIndex === -1 ? tasks.q1.length : insertIndex + 1;
    } else if (targetQuadrant === 'Q2') {
      newId = tasks.q1.length + (insertIndex === -1 ? tasks.q2.length : insertIndex + 1);
    } else if (targetQuadrant === 'Q3') {
      newId = tasks.q1.length + tasks.q2.length + (insertIndex === -1 ? tasks.q3.length : insertIndex + 1);
    } else {
      newId = tasks.q1.length + tasks.q2.length + tasks.q3.length + (insertIndex === -1 ? tasks.q4.length : insertIndex + 1);
    }

    console.log(`[Parser] Task moved successfully. New ID: ${newId}`);

    return {
      success: true,
      message: `Task moved from ${sourceQuadrant} to ${targetQuadrant}`,
      newId: newId
    };

  } catch (error) {
    console.error('[Parser] Move task error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Generate tasks.md content from parsed tasks
 * Uses task.raw to preserve original formatting, only updates task numbers
 */
function generateTasksMarkdown(tasks, originalContent) {
  // Extract header (everything before ## 🔥 Q1)
  const headerMatch = originalContent.match(/^(.*?)(?=## 🔥 Q1)/s);
  const header = headerMatch ? headerMatch[1] : '# 任务清单\n\n';

  // Calculate totals
  const totalTasks = tasks.q1.length + tasks.q2.length + tasks.q3.length + tasks.q4.length;

  // Update stats line in header (match both Chinese and English colons)
  let updatedHeader = header.replace(
    /总任务数[：:]\d+ \(Q1: \d+ \+ Q2: \d+ \+ Q3: \d+ \+ Q4: \d+\)/,
    `总任务数：${totalTasks} (Q1: ${tasks.q1.length} + Q2: ${tasks.q2.length} + Q3: ${tasks.q3.length} + Q4: ${tasks.q4.length})`
  );

  // Generate content
  let content = updatedHeader;

  // Q1
  content += '## 🔥 Q1(重要+紧急)\n\n';
  tasks.q1.forEach((task, index) => {
    content += formatTaskMarkdown(task, index + 1);
  });

  // Q2
  content += '\n## 💼 Q2(重要+不紧急)\n\n';
  tasks.q2.forEach((task, index) => {
    content += formatTaskMarkdown(task, tasks.q1.length + index + 1);
  });

  // Q3
  content += '\n## ⚡ Q3(不重要+紧急)\n\n';
  tasks.q3.forEach((task, index) => {
    content += formatTaskMarkdown(task, tasks.q1.length + tasks.q2.length + index + 1);
  });

  // Q4
  content += '\n## 🧘 Q4(不重要+不紧急)\n\n';
  tasks.q4.forEach((task, index) => {
    content += formatTaskMarkdown(task, tasks.q1.length + tasks.q2.length + tasks.q3.length + index + 1);
  });

  return content;
}

/**
 * Format a single task as markdown
 * Uses raw content if available, otherwise constructs from fields
 * Updates the task number to the new sequential number
 */
function formatTaskMarkdown(task, newId) {
  // If we have raw content, use it and just update the task number
  if (task.raw) {
    // Replace the task number in the raw content
    // Pattern: ### N. Title → ### newId. Title
    return task.raw.replace(/^### \d+\./, `### ${newId}.`);
  }

  // Fallback: construct from fields
  let md = `### ${newId}. ${task.title}`;

  if (task.tags && task.tags.length > 0) {
    md += ` [${task.tags.join('/')}]`;
  }

  md += '\n';

  if (task.priority) {
    md += `- **状态**:${task.priority}\n`;
  }
  if (task.description) {
    md += `- **描述**:${task.description}\n`;
  }
  if (task.created) {
    md += `- **创建**:${task.created}\n`;
  }
  if (task.blocked) {
    md += `- 🚫 **阻塞**\n`;
  }

  md += '\n';
  return md;
}

/**
 * Move a customer project within its customer section or between customers
 * @param {number} projectId - The project ID to move
 * @param {string} sourceCustomer - Source customer name
 * @param {string} targetCustomer - Target customer name
 * @param {number} insertIndex - Position to insert in target customer project list (-1 for append)
 */
async function moveCustomerProject(projectId, sourceCustomer, targetCustomer, insertIndex = -1) {
  try {
    console.log(`[Parser] Moving customer project ${projectId} from "${sourceCustomer}" to "${targetCustomer}" at index ${insertIndex}`);

    const customerFile = path.join(EISENHOWER_TASKS_DIR, 'customer-projects.md');
    const content = fs.readFileSync(customerFile, 'utf8');

    // Parse current customer projects
    const data = parseCustomerProjects(content);

    // Find source and target customers
    const sourceCust = data.customers.find(c => c.name === sourceCustomer);
    const targetCust = data.customers.find(c => c.name === targetCustomer);

    if (!sourceCust) {
      return { success: false, error: `Source customer "${sourceCustomer}" not found` };
    }
    if (!targetCust) {
      return { success: false, error: `Target customer "${targetCustomer}" not found` };
    }

    // Find the project to move
    const projectIndex = sourceCust.projects.findIndex(p => p.id === projectId);
    if (projectIndex === -1) {
      return { success: false, error: `Project ${projectId} not found in "${sourceCustomer}"` };
    }

    const projectToMove = sourceCust.projects[projectIndex];

    // Remove from source
    sourceCust.projects.splice(projectIndex, 1);

    // Insert into target
    if (insertIndex === -1 || insertIndex >= targetCust.projects.length) {
      targetCust.projects.push(projectToMove);
    } else {
      targetCust.projects.splice(insertIndex, 0, projectToMove);
    }

    // Generate new markdown content with renumbered projects
    const newContent = generateCustomerProjectsMarkdown(data, content);

    // Write back to file
    fs.writeFileSync(customerFile, newContent, 'utf8');

    console.log(`[Parser] Customer project moved successfully`);

    return {
      success: true,
      message: `Project moved from "${sourceCustomer}" to "${targetCustomer}"`
    };

  } catch (error) {
    console.error('[Parser] Move customer project error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Generate customer-projects.md content from parsed data
 * Rebuilds each customer section with projects in new order
 */
function generateCustomerProjectsMarkdown(data, originalContent) {
  // Update stats in overview table first
  let content = originalContent;
  const totalProjects = data.customers.reduce((sum, c) => sum + c.projects.length, 0);
  const activeProjects = data.customers.reduce((sum, c) => sum + c.projects.filter(p => !p.blocked && p.status?.toLowerCase().includes('active')).length, 0);
  const blockedProjects = data.customers.reduce((sum, c) => sum + c.projects.filter(p => p.blocked).length, 0);

  content = content.replace(
    /\|\s*🟢\s*(?:Active|进行中)\s*\|\s*\d+\s*\|/,
    `| 🟢 进行中 | ${activeProjects} |`
  );
  content = content.replace(
    /\|\s*🟡\s*(?:Blocked|阻塞中|阻塞)\s*\|\s*\d+\s*\|/,
    `| 🟡 阻塞中 | ${blockedProjects} |`
  );
  content = content.replace(
    /\*\*\s*(?:Total|总计)\s*\*\*\s*\|\s*\*\*\d+\*\*/,
    `**总计** | **${totalProjects}**`
  );

  // Process each customer: rebuild their section with projects in new order
  data.customers.forEach(customer => {
    // Find the customer section in the content
    const customerHeaderPattern = new RegExp(`(###\\s+.{1,2}\\s+优先级[\\d一二三四五]+[：:]${escapeRegex(customer.name)})(\\n*)`);
    const headerMatch = content.match(customerHeaderPattern);
    
    if (!headerMatch) return;
    
    const headerStart = headerMatch.index;
    const headerEnd = headerStart + headerMatch[0].length;
    
    // Find where this customer section ends (next ### or end of file)
    const nextCustomerMatch = content.substring(headerEnd).match(/\n###\s+.{1,2}\s+优先级/);
    const sectionEnd = nextCustomerMatch ? headerEnd + nextCustomerMatch.index : content.length;
    
    // Build new customer section content
    let newSection = headerMatch[1] + '\n\n';
    
    // Add projects in new order with updated numbers
    customer.projects.forEach((project, index) => {
      const newId = index + 1;
      if (project.raw) {
        // Update the project number in raw content
        newSection += project.raw.replace(/^#### \d+\./, `#### ${newId}.`);
      } else {
        newSection += formatCustomerProjectMarkdown(project, newId);
      }
    });
    
    // Replace the old section with new section
    content = content.substring(0, headerStart) + newSection + content.substring(sectionEnd);
  });

  return content;
}

/**
 * Format a single customer project as markdown
 * Uses raw content if available to preserve original formatting
 */
function formatCustomerProjectMarkdown(project, newId) {
  // If we have raw content, use it and just update the project number
  if (project.raw) {
    // Replace the project number in the raw content
    // Pattern: #### N. Project Name → #### newId. Project Name
    return project.raw.replace(/^#### \d+\./, `#### ${newId}.`);
  }

  // Fallback: construct from fields
  let md = `#### ${newId}. ${project.name}\n`;

  if (project.status) {
    md += `- **状态**：${project.status}\n`;
  }
  if (project.type) {
    md += `- **类型**：${project.type}\n`;
  }
  if (project.priority) {
    md += `- **优先级**：${project.priority}\n`;
  }
  if (project.created) {
    md += `- **创建时间**：${project.created}\n`;
  }
  if (project.lastReview) {
    md += `- **上次回顾**：${project.lastReview}\n`;
  }
  if (project.nextReview) {
    md += `- **下次检查**：${project.nextReview}\n`;
  }
  if (project.notes) {
    md += `- **备注**：${project.notes}\n`;
  }
  if (project.quadrantTask) {
    md += `- **象限任务**：${project.quadrantTask}\n`;
  }

  md += '\n';
  return md;
}

/**
 * Escape special regex characters
 */
function escapeRegex(string) {
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

/**
 * Delete a task from tasks.md and renumber remaining tasks
 * @param {number} taskId - The task ID to delete
 * @param {string} quadrant - The quadrant the task is in (Q1, Q2, Q3, Q4)
 */
async function deleteTask(taskId, quadrant) {
  try {
    console.log(`[Parser] Deleting task ${taskId} from ${quadrant}`);

    const tasksFile = path.join(EISENHOWER_TASKS_DIR, 'tasks.md');
    const content = fs.readFileSync(tasksFile, 'utf8');

    // Parse current tasks
    const tasks = parseTasks(content);

    // Find the task to delete
    const taskList = tasks[quadrant.toLowerCase()];
    const taskIndex = taskList.findIndex(t => t.id === taskId);

    if (taskIndex === -1) {
      return { success: false, error: `Task ${taskId} not found in ${quadrant}` };
    }

    // Remove the task
    taskList.splice(taskIndex, 1);

    // Generate new markdown with renumbered tasks
    const newContent = generateTasksMarkdown(tasks, content);

    // Write back to file
    fs.writeFileSync(tasksFile, newContent, 'utf8');

    console.log(`[Parser] Task ${taskId} deleted successfully`);

    return {
      success: true,
      message: `Task ${taskId} deleted from ${quadrant}`
    };

  } catch (error) {
    console.error('[Parser] Delete task error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Delete a customer project and renumber remaining projects
 * @param {number} projectId - The project ID to delete
 * @param {string} customerName - The customer name
 */
async function deleteCustomerProject(projectId, customerName) {
  try {
    console.log(`[Parser] Deleting project ${projectId} from "${customerName}"`);

    const customerFile = path.join(EISENHOWER_TASKS_DIR, 'customer-projects.md');
    const content = fs.readFileSync(customerFile, 'utf8');

    // Parse current customer projects
    const data = parseCustomerProjects(content);

    // Find the customer
    const customer = data.customers.find(c => c.name === customerName);
    if (!customer) {
      return { success: false, error: `Customer "${customerName}" not found` };
    }

    // Find the project to delete
    const projectIndex = customer.projects.findIndex(p => p.id === projectId);
    if (projectIndex === -1) {
      return { success: false, error: `Project ${projectId} not found in "${customerName}"` };
    }

    // Remove the project
    customer.projects.splice(projectIndex, 1);

    // Generate new markdown with renumbered projects
    const newContent = generateCustomerProjectsMarkdown(data, content);

    // Write back to file
    fs.writeFileSync(customerFile, newContent, 'utf8');

    console.log(`[Parser] Project ${projectId} deleted successfully`);

    return {
      success: true,
      message: `Project ${projectId} deleted from "${customerName}"`
    };

  } catch (error) {
    console.error('[Parser] Delete customer project error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Delete a delegation task and renumber remaining tasks
 * @param {number} taskId - The task ID to delete
 */
async function deleteDelegationTask(taskId) {
  try {
    console.log(`[Parser] Deleting delegation task ${taskId}`);

    const delegationFile = path.join(EISENHOWER_TASKS_DIR, 'delegation.md');
    const content = fs.readFileSync(delegationFile, 'utf8');

    // Parse current delegation tasks
    const data = parseDelegation(content);

    // Find the task to delete
    const taskIndex = data.tasks.findIndex(t => t.id === taskId);
    if (taskIndex === -1) {
      return { success: false, error: `Delegation task ${taskId} not found` };
    }

    // Remove the task
    data.tasks.splice(taskIndex, 1);

    // Generate new markdown with renumbered tasks
    const newContent = generateDelegationMarkdown(data, content);

    // Write back to file
    fs.writeFileSync(delegationFile, newContent, 'utf8');

    console.log(`[Parser] Delegation task ${taskId} deleted successfully`);

    return {
      success: true,
      message: `Delegation task ${taskId} deleted`
    };

  } catch (error) {
    console.error('[Parser] Delete delegation task error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Delete a maybe list task and renumber remaining tasks
 * @param {number} taskId - The task ID to delete
 */
async function deleteMaybeTask(taskId) {
  try {
    console.log(`[Parser] Deleting maybe task ${taskId}`);

    const maybeFile = path.join(EISENHOWER_TASKS_DIR, 'maybe.md');
    const content = fs.readFileSync(maybeFile, 'utf8');

    // Parse current maybe tasks
    const data = parseMaybeList(content);

    // Find the task to delete
    const taskIndex = data.tasks.findIndex(t => t.id === taskId);
    if (taskIndex === -1) {
      return { success: false, error: `Maybe task ${taskId} not found` };
    }

    // Remove the task
    data.tasks.splice(taskIndex, 1);

    // Generate new markdown with renumbered tasks
    const newContent = generateMaybeMarkdown(data, content);

    // Write back to file
    fs.writeFileSync(maybeFile, newContent, 'utf8');

    console.log(`[Parser] Maybe task ${taskId} deleted successfully`);

    return {
      success: true,
      message: `Maybe task ${taskId} deleted`
    };

  } catch (error) {
    console.error('[Parser] Delete maybe task error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Generate delegation.md content from parsed data
 */
function generateDelegationMarkdown(data, originalContent) {
  // Extract header (everything before first ####)
  const headerMatch = originalContent.match(/^(.*?)(?=####\s+\d+\.)/s);
  let content = headerMatch ? headerMatch[1] : '';

  // Update total count
  content = content.replace(/总任务数:\d+ 个/, `总任务数:${data.tasks.length} 个`);

  // Add tasks in order with updated numbers
  data.tasks.forEach((task, index) => {
    const newId = index + 1;
    if (task.raw) {
      content += task.raw.replace(/^#### \d+\./, `#### ${newId}.`);
    } else {
      content += formatDelegationTaskMarkdown(task, newId);
    }
  });

  return content;
}

/**
 * Format a single delegation task as markdown
 */
function formatDelegationTaskMarkdown(task, newId) {
  let md = `#### ${newId}. ${task.title}\n`;

  if (task.status) {
    md += `- **状态**: ${task.status}\n`;
  }
  if (task.assignee) {
    md += `- **责任人**: ${task.assignee}\n`;
  }
  if (task.description) {
    md += `- **说明**: ${task.description}\n`;
  }
  if (task.deadline) {
    md += `- **Deadline**: ${task.deadline}\n`;
  }
  if (task.created) {
    md += `- **创建时间**: ${task.created}\n`;
  }

  md += '\n';
  return md;
}

/**
 * Generate maybe.md content from parsed data
 */
function generateMaybeMarkdown(data, originalContent) {
  // Extract header (everything before first ####)
  const headerMatch = originalContent.match(/^(.*?)(?=####\s+\d+\.)/s);
  let content = headerMatch ? headerMatch[1] : '';

  // Update total count
  content = content.replace(/总任务数:\d+ 个/, `总任务数:${data.tasks.length} 个`);

  // Add tasks in order with updated numbers
  data.tasks.forEach((task, index) => {
    const newId = index + 1;
    if (task.raw) {
      content += task.raw.replace(/^#### \d+\./, `#### ${newId}.`);
    } else {
      content += formatMaybeTaskMarkdown(task, newId);
    }
  });

  return content;
}

/**
 * Format a single maybe task as markdown
 */
function formatMaybeTaskMarkdown(task, newId) {
  let md = `#### ${newId}. ${task.title}\n`;

  if (task.status) {
    md += `- **状态**: ${task.status}\n`;
  }
  if (task.description) {
    md += `- **说明**: ${task.description}\n`;
  }
  if (task.created) {
    md += `- **创建时间**: ${task.created}\n`;
  }

  md += '\n';
  return md;
}

/**
 * Copy a task from one quadrant to another
 * @param {number} taskId - The task ID to copy
 * @param {string} sourceQuadrant - Source quadrant (Q1, Q2, Q3, Q4)
 * @param {string} target - Target quadrant (Q1, Q2, Q3, Q4)
 * @returns {Object} Result object with success status and new task ID
 */
async function copyTask(taskId, sourceQuadrant, target) {
  try {
    console.log(`[Parser] Copying task ${taskId} from ${sourceQuadrant} to ${target}`);

    const tasksFile = path.join(EISENHOWER_TASKS_DIR, 'tasks.md');
    const content = fs.readFileSync(tasksFile, 'utf8');

    // Parse current tasks
    const tasks = parseTasks(content);

    // Find the source task
    const sourceList = tasks[sourceQuadrant.toLowerCase()];
    const sourceTask = sourceList.find(t => t.id === taskId);

    if (!sourceTask) {
      return { success: false, error: `Task ${taskId} not found in ${sourceQuadrant}` };
    }

    // Get target list
    const targetList = tasks[target.toLowerCase()];
    
    // Calculate new task ID (append to end of target quadrant)
    // First, calculate the starting ID for the target quadrant
    let startId = 1;
    if (target === 'Q2') startId = tasks.q1.length + 1;
    else if (target === 'Q3') startId = tasks.q1.length + tasks.q2.length + 1;
    else if (target === 'Q4') startId = tasks.q1.length + tasks.q2.length + tasks.q3.length + 1;
    
    const newTaskId = startId + targetList.length;

    // Create a copy of the task with new ID
    const copiedTask = {
      ...sourceTask,
      id: newTaskId,
      quadrant: target,
      created: new Date().toISOString().split('T')[0] // Update creation date
    };

    // Add to target list
    targetList.push(copiedTask);

    // Generate new markdown with renumbered tasks
    const newContent = generateTasksMarkdown(tasks, content);

    // Write back to file
    fs.writeFileSync(tasksFile, newContent, 'utf8');

    console.log(`[Parser] Task copied successfully as ${newTaskId}`);

    return {
      success: true,
      message: `Task copied from ${sourceQuadrant} to ${target} as #${newTaskId}`,
      newTaskId: newTaskId
    };

  } catch (error) {
    console.error('[Parser] Copy task error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Copy a customer project to quadrants or delegation list
 * @param {number} projectId - The project ID to copy
 * @param {string} sourceCustomer - Source customer name
 * @param {string} target - Target location (Q1, Q2, Q3, Q4, or 'delegation')
 * @returns {Object} Result object with success status and new task ID
 */
async function copyCustomerProject(projectId, sourceCustomer, target) {
  try {
    console.log(`[Parser] Copying project ${projectId} from "${sourceCustomer}" to ${target}`);

    const customerFile = path.join(EISENHOWER_TASKS_DIR, 'customer-projects.md');
    const customerContent = fs.readFileSync(customerFile, 'utf8');

    // Parse customer projects
    const customerData = parseCustomerProjects(customerContent);
    const sourceCust = customerData.customers.find(c => c.name === sourceCustomer);

    if (!sourceCust) {
      return { success: false, error: `Customer "${sourceCustomer}" not found` };
    }

    // Find the project
    const project = sourceCust.projects.find(p => p.id === projectId);
    if (!project) {
      return { success: false, error: `Project ${projectId} not found in "${sourceCustomer}"` };
    }

    if (target === 'delegation') {
      // Copy to delegation list
      const delegationFile = path.join(EISENHOWER_TASKS_DIR, 'delegation.md');
      const delegationContent = fs.readFileSync(delegationFile, 'utf8');
      const delegationData = parseDelegation(delegationContent);

      // Calculate new task ID
      const newTaskId = delegationData.tasks.length + 1;

      // Create delegation task from project
      const delegationTask = {
        id: newTaskId,
        title: project.name,
        status: '待开始',
        assignee: '@' + (sourceCustomer.includes('PH') ? 'wutong' : '待定'),
        description: `[${sourceCustomer}] ${project.notes || project.name}`,
        deadline: project.nextReview || '待定',
        created: new Date().toISOString().split('T')[0],
        category: sourceCustomer
      };

      delegationData.tasks.push(delegationTask);

      // Generate new markdown
      const newContent = generateDelegationMarkdown(delegationData, delegationContent);
      fs.writeFileSync(delegationFile, newContent, 'utf8');

      console.log(`[Parser] Project copied to delegation list as #${newTaskId}`);

      return {
        success: true,
        message: `Project copied to delegation list as #${newTaskId}`,
        newTaskId: newTaskId
      };

    } else {
      // Copy to quadrants (Q1, Q2, Q3, Q4)
      const tasksFile = path.join(EISENHOWER_TASKS_DIR, 'tasks.md');
      const tasksContent = fs.readFileSync(tasksFile, 'utf8');
      const tasks = parseTasks(tasksContent);

      const targetList = tasks[target.toLowerCase()];

      // Calculate new task ID
      let startId = 1;
      if (target === 'Q2') startId = tasks.q1.length + 1;
      else if (target === 'Q3') startId = tasks.q1.length + tasks.q2.length + 1;
      else if (target === 'Q4') startId = tasks.q1.length + tasks.q2.length + tasks.q3.length + 1;

      const newTaskId = startId + targetList.length;

      // Create task from project
      const quadrantTask = {
        id: newTaskId,
        title: `[${sourceCustomer}/${project.name}]`,
        tags: [sourceCustomer, project.type || 'Customer'],
        quadrant: target,
        status: 'P1',
        priority: 'P1',
        description: project.notes || `Copied from customer project: ${project.name}`,
        created: new Date().toISOString().split('T')[0],
        updated: new Date().toISOString().split('T')[0],
        blocked: project.blocked || false,
        subtasks: []
      };

      targetList.push(quadrantTask);

      // Generate new markdown
      const newContent = generateTasksMarkdown(tasks, tasksContent);
      fs.writeFileSync(tasksFile, newContent, 'utf8');

      console.log(`[Parser] Project copied to ${target} as #${newTaskId}`);

      return {
        success: true,
        message: `Project copied to ${target} as #${newTaskId}`,
        newTaskId: newTaskId
      };
    }

  } catch (error) {
    console.error('[Parser] Copy customer project error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Move a maybe task to a quadrant (cut operation)
 * @param {number} taskId - The task ID to move
 * @param {string} targetQuadrant - Target quadrant (Q1, Q2, Q3, Q4)
 * @returns {Object} Result object with success status and new task ID
 */
async function moveMaybeTaskToQuadrant(taskId, targetQuadrant) {
  try {
    console.log(`[Parser] Moving maybe task ${taskId} to ${targetQuadrant}`);

    const maybeFile = path.join(EISENHOWER_TASKS_DIR, 'maybe.md');
    const tasksFile = path.join(EISENHOWER_TASKS_DIR, 'tasks.md');
    
    // Read both files
    const maybeContent = fs.readFileSync(maybeFile, 'utf8');
    const tasksContent = fs.readFileSync(tasksFile, 'utf8');

    // Parse current data
    const maybeData = parseMaybeList(maybeContent);
    const tasks = parseTasks(tasksContent);

    // Find the maybe task to move
    const taskIndex = maybeData.tasks.findIndex(t => t.id === taskId);
    if (taskIndex === -1) {
      return { success: false, error: `Maybe task ${taskId} not found` };
    }

    const maybeTask = maybeData.tasks[taskIndex];

    // Get target list
    const targetList = tasks[targetQuadrant.toLowerCase()];

    // Calculate new task ID for target quadrant
    let startId = 1;
    if (targetQuadrant === 'Q2') startId = tasks.q1.length + 1;
    else if (targetQuadrant === 'Q3') startId = tasks.q1.length + tasks.q2.length + 1;
    else if (targetQuadrant === 'Q4') startId = tasks.q1.length + tasks.q2.length + tasks.q3.length + 1;

    const newTaskId = startId + targetList.length;

    // Create task for quadrant
    const quadrantTask = {
      id: newTaskId,
      title: maybeTask.title,
      tags: maybeTask.category ? [maybeTask.category] : [],
      quadrant: targetQuadrant,
      status: 'P1',
      priority: 'P1',
      description: maybeTask.description || `Moved from Maybe List`,
      created: new Date().toISOString().split('T')[0],
      updated: new Date().toISOString().split('T')[0],
      blocked: false,
      subtasks: []
    };

    // Add to target quadrant
    targetList.push(quadrantTask);

    // Remove from maybe list
    maybeData.tasks.splice(taskIndex, 1);

    // Generate new markdown for both files
    const newTasksContent = generateTasksMarkdown(tasks, tasksContent);
    const newMaybeContent = generateMaybeMarkdown(maybeData, maybeContent);

    // Write back to files
    fs.writeFileSync(tasksFile, newTasksContent, 'utf8');
    fs.writeFileSync(maybeFile, newMaybeContent, 'utf8');

    console.log(`[Parser] Maybe task moved to ${targetQuadrant} as #${newTaskId}`);

    return {
      success: true,
      message: `Task moved from Maybe List to ${targetQuadrant} as #${newTaskId}`,
      newTaskId: newTaskId
    };

  } catch (error) {
    console.error('[Parser] Move maybe task error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Move a task from quadrant to Maybe List
 * @param {number} taskId - The task ID to move
 * @param {string} sourceQuadrant - Source quadrant (Q1, Q2, Q3, Q4)
 * @returns {Object} Result object with success status and new task ID
 */
async function moveQuadrantTaskToMaybe(taskId, sourceQuadrant) {
  try {
    console.log(`[Parser] Moving task ${taskId} from ${sourceQuadrant} to Maybe List`);

    const tasksFile = path.join(EISENHOWER_TASKS_DIR, 'tasks.md');
    const maybeFile = path.join(EISENHOWER_TASKS_DIR, 'maybe.md');
    
    // Read both files
    const tasksContent = fs.readFileSync(tasksFile, 'utf8');
    const maybeContent = fs.readFileSync(maybeFile, 'utf8');

    // Parse current data
    const tasks = parseTasks(tasksContent);
    const maybeData = parseMaybeList(maybeContent);

    // Find the task to move
    const sourceList = tasks[sourceQuadrant.toLowerCase()];
    const taskIndex = sourceList.findIndex(t => t.id === taskId);
    
    if (taskIndex === -1) {
      return { success: false, error: `Task ${taskId} not found in ${sourceQuadrant}` };
    }

    const quadrantTask = sourceList[taskIndex];

    // Calculate new task ID for Maybe List (append to end)
    const newTaskId = maybeData.tasks.length + 1;

    // Create maybe task from quadrant task
    const maybeTask = {
      id: newTaskId,
      title: quadrantTask.title,
      status: '待评估',
      description: quadrantTask.description || `Moved from ${sourceQuadrant}`,
      created: new Date().toISOString().split('T')[0],
      category: quadrantTask.tags && quadrantTask.tags.length > 0 ? quadrantTask.tags[0] : 'General'
    };

    // Add to Maybe List
    maybeData.tasks.push(maybeTask);

    // Remove from source quadrant
    sourceList.splice(taskIndex, 1);

    // Generate new markdown for both files
    const newTasksContent = generateTasksMarkdown(tasks, tasksContent);
    const newMaybeContent = generateMaybeMarkdown(maybeData, maybeContent);

    // Write back to files
    fs.writeFileSync(tasksFile, newTasksContent, 'utf8');
    fs.writeFileSync(maybeFile, newMaybeContent, 'utf8');

    console.log(`[Parser] Task moved to Maybe List as #${newTaskId}`);

    return {
      success: true,
      message: `Task moved from ${sourceQuadrant} to Maybe List as #${newTaskId}`,
      newTaskId: newTaskId
    };

  } catch (error) {
    console.error('[Parser] Move task to maybe error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Copy a task from quadrant to Customer Project
 * @param {number} taskId - The task ID to copy
 * @param {string} sourceQuadrant - Source quadrant (Q1, Q2, Q3, Q4)
 * @param {string} targetCustomer - Target customer name
 * @returns {Object} Result object with success status and new project ID
 */
async function copyTaskToCustomer(taskId, sourceQuadrant, targetCustomer) {
  try {
    console.log(`[Parser] Copying task ${taskId} from ${sourceQuadrant} to customer "${targetCustomer}"`);

    const tasksFile = path.join(EISENHOWER_TASKS_DIR, 'tasks.md');
    const customerFile = path.join(EISENHOWER_TASKS_DIR, 'customer-projects.md');
    
    // Read both files
    const tasksContent = fs.readFileSync(tasksFile, 'utf8');
    const customerContent = fs.readFileSync(customerFile, 'utf8');

    // Parse current data
    const tasks = parseTasks(tasksContent);
    const customerData = parseCustomerProjects(customerContent);

    // Find the task to copy
    const sourceList = tasks[sourceQuadrant.toLowerCase()];
    const task = sourceList.find(t => t.id === taskId);
    
    if (!task) {
      return { success: false, error: `Task ${taskId} not found in ${sourceQuadrant}` };
    }

    // Find the target customer
    const customer = customerData.customers.find(c => c.name === targetCustomer);
    if (!customer) {
      return { success: false, error: `Customer "${targetCustomer}" not found` };
    }

    // Calculate new project ID (append to end of customer's projects)
    const newProjectId = customer.projects.length + 1;

    // Create customer project from task
    const project = {
      id: newProjectId,
      name: task.title,
      status: '🔵 待开始',
      type: '实施交付',
      priority: task.priority || '中',
      created: new Date().toISOString().split('T')[0],
      lastReview: new Date().toISOString().split('T')[0],
      notes: task.description || `Copied from ${sourceQuadrant} task #${taskId}`,
      quadrantTask: `${sourceQuadrant} #${taskId}`,
      blocked: false
    };

    // Add to customer's projects
    customer.projects.push(project);

    // Generate new markdown
    const newCustomerContent = generateCustomerProjectsMarkdown(customerData, customerContent);

    // Write back to file
    fs.writeFileSync(customerFile, newCustomerContent, 'utf8');

    console.log(`[Parser] Task copied to "${targetCustomer}" as project #${newProjectId}`);

    return {
      success: true,
      message: `Task copied to "${targetCustomer}" as project #${newProjectId}`,
      newProjectId: newProjectId
    };

  } catch (error) {
    console.error('[Parser] Copy task to customer error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Move a task from quadrant to Delegation List
 * @param {number} taskId - The task ID to move
 * @param {string} sourceQuadrant - Source quadrant (Q1, Q2, Q3, Q4)
 * @returns {Object} Result object with success status and new task ID
 */
async function moveTaskToDelegation(taskId, sourceQuadrant) {
  try {
    // Ensure taskId is a number
    const taskIdNum = parseInt(taskId, 10);
    console.log(`[Parser] Moving task ${taskIdNum} from ${sourceQuadrant} to Delegation List`);

    const tasksFile = path.join(EISENHOWER_TASKS_DIR, 'tasks.md');
    const delegationFile = path.join(EISENHOWER_TASKS_DIR, 'delegation.md');
    
    // Read both files
    const tasksContent = fs.readFileSync(tasksFile, 'utf8');
    const delegationContent = fs.readFileSync(delegationFile, 'utf8');

    // Parse current data
    const tasks = parseTasks(tasksContent);
    const delegationData = parseDelegation(delegationContent);

    // Find the task to move
    const sourceList = tasks[sourceQuadrant.toLowerCase()];
    const taskIndex = sourceList.findIndex(t => t.id === taskIdNum);
    
    if (taskIndex === -1) {
      return { success: false, error: `Task ${taskIdNum} not found in ${sourceQuadrant}` };
    }

    const quadrantTask = sourceList[taskIndex];

    // Calculate new task ID for Delegation List (append to end)
    const newTaskId = delegationData.tasks.length + 1;

    // Create delegation task from quadrant task
    const delegationTask = {
      id: newTaskId,
      title: quadrantTask.title,
      status: '待开始',
      assignee: '@待定',
      description: quadrantTask.description || `Moved from ${sourceQuadrant}`,
      deadline: '待定',
      created: new Date().toISOString().split('T')[0]
    };

    // Add to Delegation List
    delegationData.tasks.push(delegationTask);

    // Remove from source quadrant
    sourceList.splice(taskIndex, 1);

    // Generate new markdown for both files
    const newTasksContent = generateTasksMarkdown(tasks, tasksContent);
    const newDelegationContent = generateDelegationMarkdown(delegationData, delegationContent);

    // Write back to files
    fs.writeFileSync(tasksFile, newTasksContent, 'utf8');
    fs.writeFileSync(delegationFile, newDelegationContent, 'utf8');

    console.log(`[Parser] Task moved to Delegation List as #${newTaskId}`);

    return {
      success: true,
      message: `Task moved from ${sourceQuadrant} to Delegation List as #${newTaskId}`,
      newTaskId: newTaskId
    };

  } catch (error) {
    console.error('[Parser] Move task to delegation error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Copy a quadrant task to Delegation List (without removing from source)
 * @param {number} taskId - The task ID to copy
 * @param {string} sourceQuadrant - The source quadrant (Q1, Q2, Q3, Q4)
 * @returns {Object} Result object with success status
 */
async function copyTaskToDelegation(taskId, sourceQuadrant) {
  try {
    // Ensure taskId is a number
    const taskIdNum = parseInt(taskId, 10);
    console.log(`[Parser] Copying task ${taskIdNum} from ${sourceQuadrant} to Delegation List`);

    const tasksFile = path.join(EISENHOWER_TASKS_DIR, 'tasks.md');
    const delegationFile = path.join(EISENHOWER_TASKS_DIR, 'delegation.md');
    
    // Read both files
    const tasksContent = fs.readFileSync(tasksFile, 'utf8');
    const delegationContent = fs.readFileSync(delegationFile, 'utf8');

    // Parse current data
    const tasks = parseTasks(tasksContent);
    const delegationData = parseDelegation(delegationContent);

    // Find the task to copy
    const sourceList = tasks[sourceQuadrant.toLowerCase()];
    const taskIndex = sourceList.findIndex(t => t.id === taskIdNum);
    
    if (taskIndex === -1) {
      return { success: false, error: `Task ${taskIdNum} not found in ${sourceQuadrant}` };
    }

    const quadrantTask = sourceList[taskIndex];

    // Calculate new task ID for Delegation List (append to end)
    const newTaskId = delegationData.tasks.length + 1;

    // Create delegation task from quadrant task
    const delegationTask = {
      id: newTaskId,
      title: quadrantTask.title,
      status: '待开始',
      assignee: '@待定',
      description: quadrantTask.description || `Copied from ${sourceQuadrant}`,
      deadline: '待定',
      created: new Date().toISOString().split('T')[0]
    };

    // Add to Delegation List
    delegationData.tasks.push(delegationTask);

    // Generate new markdown for delegation file only (source task stays)
    const newDelegationContent = generateDelegationMarkdown(delegationData, delegationContent);

    // Write back to delegation file only
    fs.writeFileSync(delegationFile, newDelegationContent, 'utf8');

    console.log(`[Parser] Task copied to Delegation List as #${newTaskId}`);

    return {
      success: true,
      message: `Task copied from ${sourceQuadrant} to Delegation List as #${newTaskId}`,
      newTaskId: newTaskId
    };

  } catch (error) {
    console.error('[Parser] Copy task to delegation error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Reorder a delegation task within the list
 * @param {number} taskId - The task ID to reorder
 * @param {number} insertIndex - The index to insert at (-1 for end)
 * @returns {Object} Result object with success status
 */
async function reorderDelegationTask(taskId, insertIndex) {
  try {
    console.log(`[Parser] Reordering delegation task ${taskId} to index ${insertIndex}`);

    const delegationFile = path.join(EISENHOWER_TASKS_DIR, 'delegation.md');
    const content = fs.readFileSync(delegationFile, 'utf8');

    // Parse current delegation tasks
    const data = parseDelegation(content);

    // Find the task to reorder
    const taskIndex = data.tasks.findIndex(t => t.id === taskId);
    if (taskIndex === -1) {
      return { success: false, error: `Delegation task ${taskId} not found` };
    }

    // Get the task and remove it from current position
    const task = data.tasks[taskIndex];
    data.tasks.splice(taskIndex, 1);

    // Insert at new position
    if (insertIndex === -1 || insertIndex >= data.tasks.length) {
      data.tasks.push(task);
    } else {
      data.tasks.splice(insertIndex, 0, task);
    }

    // Generate new markdown with renumbered tasks
    const newContent = generateDelegationMarkdown(data, content);

    // Write back to file
    fs.writeFileSync(delegationFile, newContent, 'utf8');

    console.log(`[Parser] Delegation task ${taskId} reordered successfully`);

    return {
      success: true,
      message: `Task reordered successfully`
    };

  } catch (error) {
    console.error('[Parser] Reorder delegation task error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Reorder a maybe task within the list
 * @param {number} taskId - The task ID to reorder
 * @param {number} insertIndex - The index to insert at (-1 for end)
 * @returns {Object} Result object with success status
 */
async function reorderMaybeTask(taskId, insertIndex) {
  try {
    console.log(`[Parser] Reordering maybe task ${taskId} to index ${insertIndex}`);

    const maybeFile = path.join(EISENHOWER_TASKS_DIR, 'maybe.md');
    const content = fs.readFileSync(maybeFile, 'utf8');

    // Parse current maybe tasks
    const data = parseMaybeList(content);

    // Find the task to reorder
    const taskIndex = data.tasks.findIndex(t => t.id === taskId);
    if (taskIndex === -1) {
      return { success: false, error: `Maybe task ${taskId} not found` };
    }

    // Get the task and remove it from current position
    const task = data.tasks[taskIndex];
    data.tasks.splice(taskIndex, 1);

    // Insert at new position
    if (insertIndex === -1 || insertIndex >= data.tasks.length) {
      data.tasks.push(task);
    } else {
      data.tasks.splice(insertIndex, 0, task);
    }

    // Generate new markdown with renumbered tasks
    const newContent = generateMaybeMarkdown(data, content);

    // Write back to file
    fs.writeFileSync(maybeFile, newContent, 'utf8');

    console.log(`[Parser] Maybe task ${taskId} reordered successfully`);

    return {
      success: true,
      message: `Task reordered successfully`
    };

  } catch (error) {
    console.error('[Parser] Reorder maybe task error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Complete a task from quadrant and move to archive
 * @param {number} taskId - The task ID to complete
 * @param {string} quadrant - The quadrant (Q1, Q2, Q3, Q4)
 * @returns {Object} Result object with success status
 */
async function completeTask(taskId, quadrant) {
  try {
    console.log(`[Parser] Completing task ${taskId} from ${quadrant}`);

    const tasksFile = path.join(EISENHOWER_TASKS_DIR, 'tasks.md');
    const archiveFile = path.join(EISENHOWER_TASKS_DIR, 'archived.md');
    
    // Read files
    const tasksContent = fs.readFileSync(tasksFile, 'utf8');
    let archiveContent = fs.readFileSync(archiveFile, 'utf8');

    // Parse tasks
    const tasks = parseTasks(tasksContent);
    const sourceList = tasks[quadrant.toLowerCase()];
    
    // Find the task
    const taskIndex = sourceList.findIndex(t => t.id === taskId);
    if (taskIndex === -1) {
      return { success: false, error: `Task ${taskId} not found in ${quadrant}` };
    }

    const task = sourceList[taskIndex];
    const today = new Date().toISOString().split('T')[0];

    // Calculate next archive ID
    const archiveMatch = archiveContent.match(/####\s+(\d+)\./g);
    let nextArchiveId = 1;
    if (archiveMatch) {
      const ids = archiveMatch.map(m => parseInt(m.match(/\d+/)[0]));
      nextArchiveId = Math.max(...ids) + 1;
    }

    // Build archive entry
    const quadrantNames = {
      'Q1': '第一象限（重要且紧急）',
      'Q2': '第二象限（重要不紧急）',
      'Q3': '第三象限（不重要但紧急）',
      'Q4': '第四象限（不重要不紧急）'
    };

    let archiveEntry = `\n### ${today} 归档\n\n#### ${nextArchiveId}. ${task.title}\n- ✅ 已完成\n- 完成时间：${today}\n- 原象限：${quadrantNames[quadrant] || quadrant}\n- 原优先级：${task.priority || 'P1'}\n- 原编号：任务 #${task.id}\n`;
    if (task.description) {
      archiveEntry += `- 描述：${task.description}\n`;
    }
    if (task.created) {
      archiveEntry += `- 创建：${task.created}\n`;
    }

    // Add to archive
    archiveContent += archiveEntry;

    // Remove from source list
    sourceList.splice(taskIndex, 1);

    // Generate new tasks content
    const newTasksContent = generateTasksMarkdown(tasks, tasksContent);

    // Write files
    fs.writeFileSync(tasksFile, newTasksContent, 'utf8');
    fs.writeFileSync(archiveFile, archiveContent, 'utf8');

    console.log(`[Parser] Task ${taskId} completed and archived`);

    return {
      success: true,
      message: `Task ${taskId} completed and archived`
    };

  } catch (error) {
    console.error('[Parser] Complete task error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Complete a customer project and move to archive
 * @param {number} projectId - The project ID to complete
 * @param {string} customerName - The customer name
 * @returns {Object} Result object with success status
 */
async function completeCustomerProject(projectId, customerName) {
  try {
    console.log(`[Parser] Completing project ${projectId} from "${customerName}"`);

    const customerFile = path.join(EISENHOWER_TASKS_DIR, 'customer-projects.md');
    const archiveFile = path.join(EISENHOWER_TASKS_DIR, 'archived.md');
    
    // Read files
    const customerContent = fs.readFileSync(customerFile, 'utf8');
    let archiveContent = fs.readFileSync(archiveFile, 'utf8');

    // Parse customer projects
    const customerData = parseCustomerProjects(customerContent);
    const customer = customerData.customers.find(c => c.name === customerName);
    
    if (!customer) {
      return { success: false, error: `Customer "${customerName}" not found` };
    }

    // Find the project
    const projectIndex = customer.projects.findIndex(p => p.id === projectId);
    if (projectIndex === -1) {
      return { success: false, error: `Project ${projectId} not found in "${customerName}"` };
    }

    const project = customer.projects[projectIndex];
    const today = new Date().toISOString().split('T')[0];

    // Calculate next archive ID
    const archiveMatch = archiveContent.match(/####\s+(\d+)\./g);
    let nextArchiveId = 1;
    if (archiveMatch) {
      const ids = archiveMatch.map(m => parseInt(m.match(/\d+/)[0]));
      nextArchiveId = Math.max(...ids) + 1;
    }

    // Build archive entry
    let archiveEntry = `\n### ${today} 归档\n\n#### ${nextArchiveId}. ${project.name}【${customerName}】\n- ✅ 已完成\n- 完成时间：${today}\n- 原客户：${customerName}\n- 原编号：项目 #${project.id}\n`;
    if (project.type) {
      archiveEntry += `- 类型：${project.type}\n`;
    }
    if (project.notes) {
      archiveEntry += `- 备注：${project.notes}\n`;
    }

    // Add to archive
    archiveContent += archiveEntry;

    // Remove from customer projects
    customer.projects.splice(projectIndex, 1);

    // Generate new customer content
    const newCustomerContent = generateCustomerProjectsMarkdown(customerData, customerContent);

    // Write files
    fs.writeFileSync(customerFile, newCustomerContent, 'utf8');
    fs.writeFileSync(archiveFile, archiveContent, 'utf8');

    console.log(`[Parser] Project ${projectId} completed and archived`);

    return {
      success: true,
      message: `Project ${projectId} completed and archived`
    };

  } catch (error) {
    console.error('[Parser] Complete customer project error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Complete a delegation task and move to archive
 * @param {number} taskId - The task ID to complete
 * @returns {Object} Result object with success status
 */
async function completeDelegationTask(taskId) {
  try {
    console.log(`[Parser] Completing delegation task ${taskId}`);

    const delegationFile = path.join(EISENHOWER_TASKS_DIR, 'delegation.md');
    const archiveFile = path.join(EISENHOWER_TASKS_DIR, 'archived.md');
    
    // Read files
    const delegationContent = fs.readFileSync(delegationFile, 'utf8');
    let archiveContent = fs.readFileSync(archiveFile, 'utf8');

    // Parse delegation tasks
    const delegationData = parseDelegation(delegationContent);
    
    // Find the task
    const taskIndex = delegationData.tasks.findIndex(t => t.id === taskId);
    if (taskIndex === -1) {
      return { success: false, error: `Delegation task ${taskId} not found` };
    }

    const task = delegationData.tasks[taskIndex];
    const today = new Date().toISOString().split('T')[0];

    // Calculate next archive ID
    const archiveMatch = archiveContent.match(/####\s+(\d+)\./g);
    let nextArchiveId = 1;
    if (archiveMatch) {
      const ids = archiveMatch.map(m => parseInt(m.match(/\d+/)[0]));
      nextArchiveId = Math.max(...ids) + 1;
    }

    // Build archive entry
    let archiveEntry = `\n### ${today} 归档\n\n#### ${nextArchiveId}. ${task.title}【委派】${task.assignee ? `- ${task.assignee}` : ''}\n- ✅ 已完成\n- 完成时间：${today}\n- 原清单：委派清单\n- 原编号：任务 #${task.id}\n`;
    if (task.assignee) {
      archiveEntry += `- 责任人：${task.assignee}\n`;
    }
    if (task.description) {
      archiveEntry += `- 说明：${task.description}\n`;
    }

    // Add to archive
    archiveContent += archiveEntry;

    // Remove from delegation list
    delegationData.tasks.splice(taskIndex, 1);

    // Generate new delegation content
    const newDelegationContent = generateDelegationMarkdown(delegationData, delegationContent);

    // Write files
    fs.writeFileSync(delegationFile, newDelegationContent, 'utf8');
    fs.writeFileSync(archiveFile, archiveContent, 'utf8');

    console.log(`[Parser] Delegation task ${taskId} completed and archived`);

    return {
      success: true,
      message: `Delegation task ${taskId} completed and archived`
    };

  } catch (error) {
    console.error('[Parser] Complete delegation task error:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Complete a maybe task and move to archive
 * @param {number} taskId - The task ID to complete
 * @returns {Object} Result object with success status
 */
async function completeMaybeTask(taskId) {
  try {
    console.log(`[Parser] Completing maybe task ${taskId}`);

    const maybeFile = path.join(EISENHOWER_TASKS_DIR, 'maybe.md');
    const archiveFile = path.join(EISENHOWER_TASKS_DIR, 'archived.md');
    
    // Read files
    const maybeContent = fs.readFileSync(maybeFile, 'utf8');
    let archiveContent = fs.readFileSync(archiveFile, 'utf8');

    // Parse maybe tasks
    const maybeData = parseMaybeList(maybeContent);
    
    // Find the task
    const taskIndex = maybeData.tasks.findIndex(t => t.id === taskId);
    if (taskIndex === -1) {
      return { success: false, error: `Maybe task ${taskId} not found` };
    }

    const task = maybeData.tasks[taskIndex];
    const today = new Date().toISOString().split('T')[0];

    // Calculate next archive ID
    const archiveMatch = archiveContent.match(/####\s+(\d+)\./g);
    let nextArchiveId = 1;
    if (archiveMatch) {
      const ids = archiveMatch.map(m => parseInt(m.match(/\d+/)[0]));
      nextArchiveId = Math.max(...ids) + 1;
    }

    // Build archive entry
    let archiveEntry = `\n### ${today} 归档\n\n#### ${nextArchiveId}. ${task.title}【未来清单】\n- ✅ 已完成\n- 完成时间：${today}\n- 原清单：未来清单 (Maybe List)\n- 原编号：任务 #${task.id}\n`;
    if (task.category) {
      archiveEntry += `- 分类：${task.category}\n`;
    }
    if (task.description) {
      archiveEntry += `- 说明：${task.description}\n`;
    }

    // Add to archive
    archiveContent += archiveEntry;

    // Remove from maybe list
    maybeData.tasks.splice(taskIndex, 1);

    // Generate new maybe content
    const newMaybeContent = generateMaybeMarkdown(maybeData, maybeContent);

    // Write files
    fs.writeFileSync(maybeFile, newMaybeContent, 'utf8');
    fs.writeFileSync(archiveFile, archiveContent, 'utf8');

    console.log(`[Parser] Maybe task ${taskId} completed and archived`);

    return {
      success: true,
      message: `Maybe task ${taskId} completed and archived`
    };

  } catch (error) {
    console.error('[Parser] Complete maybe task error:', error);
    return { success: false, error: error.message };
  }
}

module.exports = {
  loadAllTasks,
  parseTasks,
  parseCustomerProjects,
  parseDelegation,
  parseMaybeList,
  moveTask,
  moveCustomerProject,
  deleteTask,
  deleteCustomerProject,
  deleteDelegationTask,
  deleteMaybeTask,
  copyTask,
  copyCustomerProject,
  moveMaybeTaskToQuadrant,
  moveQuadrantTaskToMaybe,
  copyTaskToCustomer,
  moveTaskToDelegation,
  copyTaskToDelegation,
  reorderDelegationTask,
  reorderMaybeTask,
  completeTask,
  completeCustomerProject,
  completeDelegationTask,
  completeMaybeTask
};


path: dashboard/public/app.js
content: /**
 * Eisenhower Task Dashboard - Frontend Application
 * Handles UI rendering and WebSocket communication
 * Supports internationalization (i18n)
 */

// Global state
let currentData = null;
let ws = null;
let currentTab = 'matrix';
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;

// Drag and drop state
let draggedTask = null;
let draggedSourceQuadrant = null;
let dropIndicator = null;

// Customer project drag and drop state
let draggedCustomerProject = null;
let draggedSourceCustomer = null;

// Initialize application
document.addEventListener('DOMContentLoaded', () => {
  initTabs();
  initFilters();
  initLanguageSwitcher();
  connectWebSocket();
  // Initial data load via fetch as fallback
  fetchInitialData();
});

// Initialize language switcher
function initLanguageSwitcher() {
  const langBtn = document.getElementById('langBtn');
  const langDropdown = document.getElementById('langDropdown');

  if (!langBtn || !langDropdown) return;

  // Set initial button state based on current language
  updateLangButton(i18n.getLanguage());

  // Toggle dropdown
  langBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    langDropdown.classList.toggle('show');
  });

  // Close dropdown when clicking outside
  document.addEventListener('click', () => {
    langDropdown.classList.remove('show');
  });

  // Language selection
  langDropdown.querySelectorAll('.lang-option').forEach(option => {
    option.addEventListener('click', () => {
      const lang = option.dataset.lang;
      if (i18n.setLanguage(lang)) {
        updateLangButton(lang);
        // Re-render dynamic content
        renderAll();
      }
    });
  });
}

// Update language button display
function updateLangButton(lang) {
  const langBtn = document.getElementById('langBtn');
  const langText = langBtn.querySelector('.lang-text');
  langText.textContent = lang === 'zh-CN' ? '中文' : 'EN';
  langBtn.dataset.current = lang;
}

// Tab switching
function initTabs() {
  const tabBtns = document.querySelectorAll('.tab-btn');
  const tabContents = document.querySelectorAll('.tab-content');

  tabBtns.forEach(btn => {
    btn.addEventListener('click', () => {
      const tabId = btn.dataset.tab;
      currentTab = tabId;

      // Update active states
      tabBtns.forEach(b => b.classList.remove('active'));
      tabContents.forEach(c => c.classList.remove('active'));

      btn.classList.add('active');
      document.getElementById(tabId).classList.add('active');
    });
  });
}

// Filter buttons
function initFilters() {
  // Customer filters
  const customerFilters = document.querySelectorAll('#customers .filter-btn');
  customerFilters.forEach(btn => {
    btn.addEventListener('click', () => {
      customerFilters.forEach(b => b.classList.remove('active'));
      btn.classList.add('active');
      renderCustomers(currentData?.customerProjects, btn.dataset.filter);
    });
  });

  // Delegation filters
  const delegationFilters = document.querySelectorAll('#delegation .filter-btn');
  delegationFilters.forEach(btn => {
    btn.addEventListener('click', () => {
      delegationFilters.forEach(b => b.classList.remove('active'));
      btn.classList.add('active');
      renderDelegation(currentData?.delegation, btn.dataset.filter);
    });
  });
}

// WebSocket connection
function connectWebSocket() {
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  const wsUrl = `${protocol}//${window.location.host}`;

  updateConnectionStatus('connecting');

  ws = new WebSocket(wsUrl);

  ws.onopen = () => {
    console.log('[WebSocket] Connected');
    updateConnectionStatus('connected');
    reconnectAttempts = 0;
  };

  ws.onmessage = (event) => {
    try {
      const message = JSON.parse(event.data);
      if (message.type === 'init' || message.type === 'update') {
        currentData = message.data;
        renderAll();
      }
    } catch (e) {
      console.error('[WebSocket] Parse error:', e);
    }
  };

  ws.onclose = () => {
    console.log('[WebSocket] Disconnected');
    updateConnectionStatus('disconnected');

    // Attempt reconnection
    if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
      reconnectAttempts++;
      setTimeout(connectWebSocket, 3000 * reconnectAttempts);
    }
  };

  ws.onerror = (error) => {
    console.error('[WebSocket] Error:', error);
    updateConnectionStatus('disconnected');
  };
}

// Update connection status indicator
function updateConnectionStatus(status) {
  const statusEl = document.getElementById('connectionStatus');
  const dot = statusEl.querySelector('.status-dot');
  const text = statusEl.querySelector('.status-text');

  dot.className = 'status-dot';

  switch (status) {
    case 'connected':
      dot.classList.add('connected');
      text.textContent = i18n.t('status_live');
      break;
    case 'disconnected':
      dot.classList.add('disconnected');
      text.textContent = i18n.t('status_offline');
      break;
    default:
      text.textContent = i18n.t('status_connecting');
  }
}

// Fetch initial data via HTTP
async function fetchInitialData() {
  try {
    const response = await fetch('/api/tasks');
    if (!response.ok) throw new Error('HTTP ' + response.status);

    const data = await response.json();
    currentData = data;
    renderAll();
  } catch (e) {
    console.error('[Fetch] Error:', e);
  }
}

// Render all components
function renderAll() {
  if (!currentData) return;

  renderStats();
  renderMatrix();
  renderCustomers(currentData.customerProjects);
  renderDelegation(currentData.delegation);
  renderMaybe(currentData.maybe);
  updateTimestamp();
}

// Update stats bar
function renderStats() {
  const tasks = currentData.tasks;
  const customers = currentData.customerProjects;
  const delegation = currentData.delegation;
  const maybe = currentData.maybe;

  document.getElementById('q1Count').textContent = tasks?.stats?.q1 || 0;
  document.getElementById('q2Count').textContent = tasks?.stats?.q2 || 0;
  document.getElementById('q3Count').textContent = tasks?.stats?.q3 || 0;
  document.getElementById('q4Count').textContent = tasks?.stats?.q4 || 0;
  document.getElementById('customerCount').textContent = customers?.stats?.total || 0;
  document.getElementById('delegationCount').textContent = delegation?.stats?.total || 0;
  document.getElementById('maybeCount').textContent = maybe?.stats?.total || 0;
}

// Render quadrant matrix
function renderMatrix() {
  const tasks = currentData.tasks;
  if (!tasks) return;

  renderTaskList('q1List', tasks.q1, 'Q1');
  renderTaskList('q2List', tasks.q2, 'Q2');
  renderTaskList('q3List', tasks.q3, 'Q3');
  renderTaskList('q4List', tasks.q4, 'Q4');
}

// Store tooltip elements keyed by task ID for proper cleanup
const activeTooltips = new Map();

// Render a task list
function renderTaskList(elementId, tasks, quadrant) {
  const container = document.getElementById(elementId);
  if (!container) return;

  // Clean up existing tooltips for this container
  container.querySelectorAll('.task-card').forEach(card => {
    const taskId = card.dataset.taskId;
    if (activeTooltips.has(taskId)) {
      const tooltip = activeTooltips.get(taskId);
      if (tooltip && tooltip.parentNode) {
        tooltip.parentNode.removeChild(tooltip);
      }
      activeTooltips.delete(taskId);
    }
  });

  if (!tasks || tasks.length === 0) {
    container.innerHTML = `<div class="empty-state">${i18n.t('empty_tasks')}</div>`;
    // Add drop zone handling even for empty lists
    initDragAndDrop(container, quadrant);
    return;
  }

  container.innerHTML = tasks.map(task => `
    <div class="task-card ${task.blocked ? 'blocked' : ''} ${task.priority ? 'priority-' + task.priority.toLowerCase() : ''}" 
         data-task-id="${task.id}" 
         data-quadrant="${quadrant}">
      <div class="drag-handle" draggable="true" title="Drag to move"></div>
      <button class="copy-btn" data-task-id="${task.id}" data-source="${quadrant}" data-type="quadrant" title="Copy to...">⎘</button>
      <button class="move-btn" data-task-id="${task.id}" data-source="${quadrant}" data-type="quadrant-to-maybe" title="Move to Maybe...">→</button>
      <button class="complete-btn" data-task-id="${task.id}" data-quadrant="${quadrant}" title="Complete">✓</button>
      <button class="delete-btn" data-task-id="${task.id}" data-quadrant="${quadrant}" title="Delete">×</button>
      <div class="task-title-row">
        <span class="task-id">#${task.id}</span>
        ${task.priority ? `<span class="task-priority ${task.priority.toLowerCase()}">${task.priority}</span>` : ''}
        <span class="task-title-text">${escapeHtml(task.title)}</span>
      </div>
      ${task.description ? `<div class="task-desc">${escapeHtml(task.description)}</div>` : ''}
      ${task.tags.length > 0 ? `
        <div class="task-tags">
          ${task.tags.map(tag => `<span class="task-tag">${escapeHtml(tag)}</span>`).join('')}
        </div>
      ` : ''}
      ${task.blocked ? `<div class="task-meta"><span class="blocked-badge">${i18n.t('blocked_badge')}</span></div>` : ''}
    </div>
  `).join('');

  // Initialize drag and drop for this container
  initDragAndDrop(container, quadrant);

  // Add complete button event listeners
  container.querySelectorAll('.complete-btn').forEach(btn => {
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      const taskId = parseInt(btn.dataset.taskId);
      const taskQuadrant = btn.dataset.quadrant;
      showConfirm(`Complete task #${taskId}?`, () => {
        completeTask(taskId, taskQuadrant);
      });
    });
  });

  // Add delete button event listeners
  container.querySelectorAll('.delete-btn').forEach(btn => {
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      const taskId = parseInt(btn.dataset.taskId);
      const taskQuadrant = btn.dataset.quadrant;
      showConfirm(`Delete task #${taskId}?`, () => {
        deleteTask(taskId, taskQuadrant);
      });
    });
  });

  // Add copy button event listeners
  container.querySelectorAll('.copy-btn').forEach(btn => {
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      const taskId = parseInt(btn.dataset.taskId);
      const source = btn.dataset.source;
      const type = btn.dataset.type;
      showCopyMenu(taskId, source, type);
    });
  });

  // Add move button event listeners for moving to Maybe or Delegation
  container.querySelectorAll('.move-btn').forEach(btn => {
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      const taskId = parseInt(btn.dataset.taskId);
      const source = btn.dataset.source;
      showMoveToMenu(taskId, source);
    });
  });

  // Add hover event listeners for tooltips
  container.querySelectorAll('.task-card').forEach((card, index) => {
    const task = tasks[index];
    if (!task) return;

    // Create tooltip element and append to body to avoid CSS containment issues
    const tooltip = document.createElement('div');
    tooltip.className = 'task-tooltip';
    tooltip.innerHTML = `
      <div class="tooltip-header">
        <span class="tooltip-id">#${task.id}</span>
        ${task.priority ? `<span class="tooltip-priority ${task.priority.toLowerCase()}">${task.priority}</span>` : ''}
        ${task.blocked ? `<span class="tooltip-blocked">🚫 ${i18n.t('blocked_badge')}</span>` : ''}
      </div>
      <div class="tooltip-title">${escapeHtml(task.title)}</div>
      ${task.description ? `<div class="tooltip-section"><div class="tooltip-label">${i18n.t('description')}</div><div class="tooltip-desc">${escapeHtml(task.description)}</div></div>` : ''}
      ${task.subtasks && task.subtasks.length > 0 ? `
        <div class="tooltip-section">
          <div class="tooltip-label">${i18n.t('subtasks')} (${task.subtasks.length})</div>
          <div class="tooltip-subtasks">
            ${task.subtasks.map(sub => `<div class="tooltip-subtask">• ${escapeHtml(sub.title)}${sub.content ? `<br><small>${escapeHtml(sub.content)}</small>` : ''}</div>`).join('')}
          </div>
        </div>
      ` : ''}
      ${task.tags.length > 0 ? `
        <div class="tooltip-section">
          <div class="tooltip-label">${i18n.t('tags')}</div>
          <div class="tooltip-tags">
            ${task.tags.map(tag => `<span class="tooltip-tag">${escapeHtml(tag)}</span>`).join('')}
          </div>
        </div>
      ` : ''}
      <div class="tooltip-meta">
        ${task.created ? `<div><span class="tooltip-label">${i18n.t('created_prefix')}</span> ${task.created}</div>` : ''}
        ${task.updated ? `<div><span class="tooltip-label">${i18n.t('updated_prefix')}</span> ${task.updated}</div>` : ''}
      </div>
    `;
    document.body.appendChild(tooltip);
    activeTooltips.set(String(task.id), tooltip);

    card.addEventListener('mouseenter', (e) => {
      // First show tooltip to get its dimensions
      tooltip.classList.add('show');
      
      // Then calculate position based on actual dimensions
      const tooltipRect = tooltip.getBoundingClientRect();
      const viewportWidth = window.innerWidth;
      const viewportHeight = window.innerHeight;

      let left = e.clientX + 15;
      let top = e.clientY + 15;

      // Adjust if tooltip goes off right edge
      if (left + tooltipRect.width > viewportWidth) {
        left = e.clientX - tooltipRect.width - 15;
      }

      // Adjust if tooltip goes off bottom edge
      if (top + tooltipRect.height > viewportHeight) {
        top = e.clientY - tooltipRect.height - 15;
      }

      tooltip.style.left = left + 'px';
      tooltip.style.top = top + 'px';
    });

    card.addEventListener('mouseleave', () => {
      tooltip.classList.remove('show');
    });

    card.addEventListener('mousemove', (e) => {
      const tooltipRect = tooltip.getBoundingClientRect();
      const viewportWidth = window.innerWidth;
      const viewportHeight = window.innerHeight;

      // Calculate position relative to viewport
      let left = e.clientX + 15;
      let top = e.clientY + 15;

      // Adjust if tooltip goes off right edge
      if (left + tooltipRect.width > viewportWidth) {
        left = e.clientX - tooltipRect.width - 15;
      }

      // Adjust if tooltip goes off bottom edge
      if (top + tooltipRect.height > viewportHeight) {
        top = e.clientY - tooltipRect.height - 15;
      }

      tooltip.style.left = left + 'px';
      tooltip.style.top = top + 'px';
    });
  });

  // Initialize drag and drop for customer project lists
  container.querySelectorAll('.projects-list').forEach(projectList => {
    initCustomerDragAndDrop(projectList);
  });
}

// Track which containers have been initialized for drag and drop
const dndInitializedContainers = new Set();

// Track which customer project containers have been initialized for drag and drop
const customerDndInitializedContainers = new Set();

// Initialize drag and drop for a task list
function initDragAndDrop(container, quadrant) {
  // Only initialize container-level events once
  if (!dndInitializedContainers.has(container.id)) {
    dndInitializedContainers.add(container.id);
    container.addEventListener('dragover', handleDragOver);
    container.addEventListener('dragenter', handleDragEnter);
    container.addEventListener('dragleave', handleDragLeave);
    container.addEventListener('drop', (e) => handleDrop(e, quadrant));
  }

  // Make drag handles draggable (these need to be re-attached after each render)
  container.querySelectorAll('.drag-handle').forEach(handle => {
    // Remove existing listeners to prevent duplicates
    handle.removeEventListener('dragstart', handleDragStart);
    handle.removeEventListener('dragend', handleDragEnd);
    // Add listeners
    handle.addEventListener('dragstart', handleDragStart);
    handle.addEventListener('dragend', handleDragEnd);
  });
}

// Drag start handler
function handleDragStart(e) {
  // Find the parent task card from the drag handle
  const card = e.target.closest('.task-card');
  if (!card) return;
  
  draggedTask = {
    id: parseInt(card.dataset.taskId),
    quadrant: card.dataset.quadrant,
    element: card
  };
  draggedSourceQuadrant = card.dataset.quadrant;
  
  card.classList.add('dragging');
  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/plain', JSON.stringify({
    id: draggedTask.id,
    quadrant: draggedTask.quadrant
  }));
  
  console.log('[Drag] Started:', draggedTask);
}

// Drag end handler
function handleDragEnd(e) {
  // Remove dragging class from the parent card
  const card = e.target.closest('.task-card');
  if (card) {
    card.classList.remove('dragging');
  }
  
  // Clean up drop indicators and drop zone highlighting
  document.querySelectorAll('.drop-indicator').forEach(el => el.remove());
  document.querySelectorAll('.task-list').forEach(el => {
    el.classList.remove('drag-over', 'q1-list', 'q2-list', 'q3-list', 'q4-list');
  });
  
  // Clear drag state (drop handler may have already processed the move)
  draggedTask = null;
  draggedSourceQuadrant = null;
  console.log('[Drag] Ended');
}

// Drag enter handler
function handleDragEnter(e) {
  e.preventDefault();
  const container = e.currentTarget;
  const quadrant = container.id.replace('List', '').toUpperCase();
  container.classList.add('drag-over', `${quadrant.toLowerCase()}-list`);
}

// Drag leave handler
function handleDragLeave(e) {
  const container = e.currentTarget;
  // Only remove if we're actually leaving the container (not entering a child)
  if (!container.contains(e.relatedTarget)) {
    container.classList.remove('drag-over', 'q1-list', 'q2-list', 'q3-list', 'q4-list');
  }
}

// Drag over handler - for reordering within list
function handleDragOver(e) {
  e.preventDefault();
  e.dataTransfer.dropEffect = 'move';
  
  if (!draggedTask) return;
  
  const container = e.currentTarget;
  const afterElement = getDragAfterElement(container, e.clientY);
  
  // Update drop indicator
  document.querySelectorAll('.drop-indicator').forEach(el => el.remove());
  
  const indicator = document.createElement('div');
  indicator.className = 'drop-indicator';
  
  if (afterElement == null) {
    container.appendChild(indicator);
  } else {
    container.insertBefore(indicator, afterElement);
  }
}

// Get the element after which to insert the dragged element
function getDragAfterElement(container, y, selector = '.task-card', x = null) {
  const draggableElements = [...container.querySelectorAll(`${selector}:not(.dragging)`)];

  // For grid layout, we need to consider 2D position
  // Find the element that is closest to the cursor in grid order
  return draggableElements.reduce((closest, child) => {
    const box = child.getBoundingClientRect();

    // Calculate distance to the center of the element
    const centerX = box.left + box.width / 2;
    const centerY = box.top + box.height / 2;

    // For grid layout, check if cursor is before this element in reading order
    // (top to bottom, left to right)
    let isBefore;
    if (x !== null) {
      // Grid layout: check row first, then column
      const sameRow = Math.abs(y - centerY) < box.height / 2;
      if (sameRow) {
        isBefore = x < centerX;
      } else {
        isBefore = y < box.top;
      }
    } else {
      // List layout: only check Y position
      isBefore = y < box.top + box.height / 2;
    }

    // Calculate a score based on proximity
    const dx = x !== null ? x - centerX : 0;
    const dy = y - centerY;
    const distance = Math.sqrt(dx * dx + dy * dy);

    if (isBefore && distance < closest.distance) {
      return { distance: distance, element: child };
    } else {
      return closest;
    }
  }, { distance: Number.POSITIVE_INFINITY }).element;
}

// Track if a drop is currently being processed
let isProcessingDrop = false;

// Drop handler
async function handleDrop(e, targetQuadrant) {
  e.preventDefault();
  
  // Prevent duplicate drop processing
  if (isProcessingDrop) {
    console.log('[Drop] Already processing a drop, ignoring duplicate');
    return;
  }
  isProcessingDrop = true;
  
  const container = e.currentTarget;
  container.classList.remove('drag-over', 'q1-list', 'q2-list', 'q3-list', 'q4-list');
  
  // Clean up drop indicators
  document.querySelectorAll('.drop-indicator').forEach(el => el.remove());
  
  if (!draggedTask) {
    isProcessingDrop = false;
    return;
  }
  
  const sourceQuadrant = draggedTask.quadrant;
  const taskId = draggedTask.id;
  const draggedElement = draggedTask.element;
  
  // Find the insert position
  const afterElement = getDragAfterElement(container, e.clientY);
  let insertIndex = -1; // -1 means append at end
  
  if (afterElement) {
    const allCards = [...container.querySelectorAll('.task-card')];
    // Filter out the dragging card
    const visibleCards = allCards.filter(c => !c.classList.contains('dragging'));
    insertIndex = visibleCards.indexOf(afterElement);
  }
  
  console.log('[Drop] Task:', taskId, 'from', sourceQuadrant, 'to', targetQuadrant, 'at index', insertIndex);
  
  // OPTIMISTIC UPDATE: Immediately move the DOM element
  // Remove dragging class first
  draggedElement.classList.remove('dragging');
  
  // Move element to new position in DOM
  if (afterElement) {
    container.insertBefore(draggedElement, afterElement);
  } else {
    container.appendChild(draggedElement);
  }
  
  // Update the data-quadrant attribute
  draggedElement.dataset.quadrant = targetQuadrant;
  
  // Note: We do NOT renumber tasks here because the API needs the original taskId
  // The server will handle renumbering and we'll refresh after API call
  
  // Call API to persist changes
  try {
    const response = await fetch('/api/tasks/move', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        taskId: taskId,
        sourceQuadrant: sourceQuadrant,
        targetQuadrant: targetQuadrant,
        insertIndex: insertIndex
      })
    });
    
    if (!response.ok) {
      throw new Error('Failed to move task: ' + response.statusText);
    }
    
    const result = await response.json();
    console.log('[Drop] Success:', result);
    
    // Refresh data from server to ensure consistency
    await fetchInitialData();
    
  } catch (error) {
    console.error('[Drop] Error:', error);
    alert('Failed to move task: ' + error.message);
    // On error, refresh to get correct state from server
    await fetchInitialData();
  } finally {
    // Reset processing flag
    isProcessingDrop = false;
  }
}

// Initialize drag and drop for customer project lists
function initCustomerDragAndDrop(projectList) {
  const customerName = projectList.dataset.customer;

  // Always bind container-level events (elements are recreated on each render)
  projectList.addEventListener('dragover', handleCustomerDragOver);
  projectList.addEventListener('dragenter', handleCustomerDragEnter);
  projectList.addEventListener('dragleave', handleCustomerDragLeave);
  projectList.addEventListener('drop', (e) => handleCustomerDrop(e, customerName));

  // Make drag handles draggable
  projectList.querySelectorAll('.customer-drag-handle').forEach(handle => {
    handle.draggable = true;
    handle.removeEventListener('dragstart', handleCustomerDragStart);
    handle.removeEventListener('dragend', handleCustomerDragEnd);
    handle.addEventListener('dragstart', handleCustomerDragStart);
    handle.addEventListener('dragend', handleCustomerDragEnd);
  });
}

// Customer project drag start handler
function handleCustomerDragStart(e) {
  const projectItem = e.target.closest('.project-item');
  if (!projectItem) return;

  draggedCustomerProject = {
    id: parseInt(projectItem.dataset.projectId),
    customer: projectItem.dataset.customer,
    element: projectItem
  };
  draggedSourceCustomer = projectItem.dataset.customer;

  projectItem.classList.add('dragging');
  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/plain', JSON.stringify({
    id: draggedCustomerProject.id,
    customer: draggedCustomerProject.customer
  }));

  console.log('[Customer Drag] Started:', draggedCustomerProject);
}

// Customer project drag end handler
function handleCustomerDragEnd(e) {
  const projectItem = e.target.closest('.project-item');
  if (projectItem) {
    projectItem.classList.remove('dragging');
  }

  // Clean up
  document.querySelectorAll('.drop-indicator').forEach(el => el.remove());
  document.querySelectorAll('.projects-list').forEach(el => {
    el.classList.remove('drag-over');
  });

  draggedCustomerProject = null;
  draggedSourceCustomer = null;
  console.log('[Customer Drag] Ended');
}

// Customer project drag leave handler
function handleCustomerDragLeave(e) {
  const projectList = e.currentTarget;
  if (!projectList.contains(e.relatedTarget)) {
    projectList.classList.remove('drag-over');
  }
}

// Customer project drag enter handler
function handleCustomerDragEnter(e) {
  e.preventDefault();
  const projectList = e.currentTarget;
  projectList.classList.add('drag-over');
}

// Customer project drag over handler
function handleCustomerDragOver(e) {
  e.preventDefault();
  e.dataTransfer.dropEffect = 'move';

  if (!draggedCustomerProject) return;

  const projectList = e.currentTarget;
  const afterElement = getDragAfterElementForProjects(projectList, e.clientY);

  // Update drop indicator
  document.querySelectorAll('.drop-indicator').forEach(el => el.remove());

  const indicator = document.createElement('div');
  indicator.className = 'drop-indicator customer-drop-indicator';

  if (afterElement == null) {
    projectList.appendChild(indicator);
  } else {
    projectList.insertBefore(indicator, afterElement);
  }
}





// Get the element after which to insert the dragged project
function getDragAfterElementForProjects(container, y) {
  const draggableElements = [...container.querySelectorAll('.project-item:not(.dragging)')];

  return draggableElements.reduce((closest, child) => {
    const box = child.getBoundingClientRect();
    const offset = y - box.top - box.height / 2;

    if (offset < 0 && offset > closest.offset) {
      return { offset: offset, element: child };
    } else {
      return closest;
    }
  }, { offset: Number.NEGATIVE_INFINITY }).element;
}

// Track if a customer project drop is being processed
let isProcessingCustomerDrop = false;

// Customer project drop handler
async function handleCustomerDrop(e, targetCustomer) {
  e.preventDefault();

  if (isProcessingCustomerDrop) {
    console.log('[Customer Drop] Already processing, ignoring duplicate');
    return;
  }
  isProcessingCustomerDrop = true;

  const projectList = e.currentTarget;
  projectList.classList.remove('drag-over');

  // Clean up drop indicators
  document.querySelectorAll('.drop-indicator').forEach(el => el.remove());

  if (!draggedCustomerProject) {
    isProcessingCustomerDrop = false;
    return;
  }

  const sourceCustomer = draggedCustomerProject.customer;
  const projectId = draggedCustomerProject.id;
  const draggedElement = draggedCustomerProject.element;

  // Find insert position
  const afterElement = getDragAfterElementForProjects(projectList, e.clientY);
  let insertIndex = -1;

  if (afterElement) {
    const allItems = [...projectList.querySelectorAll('.project-item')];
    const visibleItems = allItems.filter(i => !i.classList.contains('dragging'));
    insertIndex = visibleItems.indexOf(afterElement);
  }

  console.log('[Customer Drop] Project:', projectId, 'from', sourceCustomer, 'to', targetCustomer, 'at index', insertIndex);

  // Optimistic update
  draggedElement.classList.remove('dragging');

  if (afterElement) {
    projectList.insertBefore(draggedElement, afterElement);
  } else {
    projectList.appendChild(draggedElement);
  }

  // Update data attribute
  draggedElement.dataset.customer = targetCustomer;

  // Call API
  try {
    const response = await fetch('/api/customer-projects/move', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        projectId: projectId,
        sourceCustomer: sourceCustomer,
        targetCustomer: targetCustomer,
        insertIndex: insertIndex
      })
    });

    if (!response.ok) {
      throw new Error('Failed to move project: ' + response.statusText);
    }

    const result = await response.json();
    console.log('[Customer Drop] Success:', result);

    await fetchInitialData();

  } catch (error) {
    console.error('[Customer Drop] Error:', error);
    alert('Failed to move project: ' + error.message);
    await fetchInitialData();
  } finally {
    isProcessingCustomerDrop = false;
  }
}

// Render customer projects
function renderCustomers(data, filter = 'all') {
  const container = document.getElementById('customerList');
  if (!container || !data) return;

  const customers = data.customers || [];

  if (customers.length === 0) {
    container.innerHTML = `<div class="empty-state">${i18n.t('empty_customer')}</div>`;
    return;
  }

  // Clear any existing customer tooltips
  document.querySelectorAll('.customer-tooltip').forEach(t => t.remove());

  container.innerHTML = customers.map(customer => {
    // Filter projects
    let projects = customer.projects || [];
    if (filter === 'active') {
      projects = projects.filter(p => !p.blocked && p.status.toLowerCase().includes('active'));
    } else if (filter === 'blocked') {
      projects = projects.filter(p => p.blocked);
    }

    if (projects.length === 0) return '';

    return `
      <div class="customer-card" data-customer="${escapeHtml(customer.name)}">
        <div class="customer-header">
          <div class="customer-name">${escapeHtml(customer.name)}</div>
          <div class="customer-priority">${escapeHtml(customer.priority)}</div>
        </div>
        <div class="projects-list" data-customer="${escapeHtml(customer.name)}">
          ${projects.map(project => `
            <div class="project-item ${project.blocked ? 'blocked' : ''}" data-project-id="${project.id}" data-customer="${escapeHtml(customer.name)}">
              <div class="drag-handle customer-drag-handle" draggable="true" title="${i18n.t('drag_to_reorder') || 'Drag to reorder'}"></div>
              <button class="copy-btn customer-copy-btn" data-task-id="${project.id}" data-source="${escapeHtml(customer.name)}" data-type="customer" title="Copy to...">⎘</button>
              <button class="complete-btn customer-complete-btn" data-project-id="${project.id}" data-customer="${escapeHtml(customer.name)}" title="Complete">✓</button>
              <button class="delete-btn customer-delete-btn" data-project-id="${project.id}" data-customer="${escapeHtml(customer.name)}" title="Delete">×</button>
              <div class="project-content">
                <div class="project-header">
                  <div class="project-name"><span class="task-id">#${project.id}</span> ${escapeHtml(project.name)}</div>
                  <div class="project-status ${project.blocked ? 'blocked' : project.status.toLowerCase().replace(/\s+/g, '')}">
                    ${project.blocked ? '🟡 ' + i18n.t('status_blocked') : escapeHtml(project.status)}
                  </div>
                </div>
                <div class="project-meta">
                  <span>${i18n.t('type_prefix')} ${escapeHtml(project.type)}</span>
                  <span>${i18n.t('priority_prefix')} ${escapeHtml(project.priority)}</span>
                  ${project.lastReview ? `<span>${i18n.t('reviewed_prefix')} ${escapeHtml(project.lastReview)}</span>` : ''}
                </div>
                ${project.notes ? `<div class="project-notes">${escapeHtml(project.notes)}</div>` : ''}
              </div>
            </div>
          `).join('')}
        </div>
      </div>
    `;
  }).filter(Boolean).join('') || `<div class="empty-state">${i18n.t('empty_filtered')}</div>`;

  // Add hover tooltips for project items
  container.querySelectorAll('.project-item').forEach(item => {
    const customerCard = item.closest('.customer-card');
    const customerName = customerCard?.dataset.customer || '';
    const projectId = item.dataset.projectId;
    
    // Find project data
    let project = null;
    for (const customer of customers) {
      project = customer.projects?.find(p => p.id.toString() === projectId);
      if (project) break;
    }
    if (!project) return;

    const tooltip = document.createElement('div');
    tooltip.className = 'task-tooltip customer-tooltip';
    tooltip.innerHTML = `
      <div class="tooltip-header">
        <span class="tooltip-id">#${project.id}</span>
        <span class="tooltip-priority">${escapeHtml(customerName)}</span>
      </div>
      <div class="tooltip-title">${escapeHtml(project.name)}</div>
      <div class="tooltip-section">
        <div class="tooltip-label">${i18n.t('status_active')}</div>
        <div class="tooltip-desc">${project.blocked ? '🟡 ' + i18n.t('status_blocked') : escapeHtml(project.status)}</div>
      </div>
      <div class="tooltip-section">
        <div class="tooltip-label">${i18n.t('type_prefix')}</div>
        <div class="tooltip-desc">${escapeHtml(project.type)}</div>
      </div>
      <div class="tooltip-section">
        <div class="tooltip-label">${i18n.t('priority_prefix')}</div>
        <div class="tooltip-desc">${escapeHtml(project.priority)}</div>
      </div>
      ${project.notes ? `<div class="tooltip-section"><div class="tooltip-label">${i18n.t('description')}</div><div class="tooltip-desc">${escapeHtml(project.notes)}</div></div>` : ''}
      <div class="tooltip-meta">
        ${project.created ? `<div><span class="tooltip-label">${i18n.t('created_prefix')}</span> ${project.created}</div>` : ''}
        ${project.lastReview ? `<div><span class="tooltip-label">${i18n.t('reviewed_prefix')}</span> ${project.lastReview}</div>` : ''}
      </div>
    `;
    document.body.appendChild(tooltip);

    item.addEventListener('mouseenter', (e) => {
      tooltip.classList.add('show');
      const tooltipRect = tooltip.getBoundingClientRect();
      const viewportWidth = window.innerWidth;
      const viewportHeight = window.innerHeight;
      let left = e.clientX + 15;
      let top = e.clientY + 15;
      if (left + tooltipRect.width > viewportWidth) left = e.clientX - tooltipRect.width - 15;
      if (top + tooltipRect.height > viewportHeight) top = e.clientY - tooltipRect.height - 15;
      tooltip.style.left = left + 'px';
      tooltip.style.top = top + 'px';
    });

    item.addEventListener('mouseleave', () => tooltip.classList.remove('show'));

    item.addEventListener('mousemove', (e) => {
      const tooltipRect = tooltip.getBoundingClientRect();
      const viewportWidth = window.innerWidth;
      const viewportHeight = window.innerHeight;
      let left = e.clientX + 15;
      let top = e.clientY + 15;
      if (left + tooltipRect.width > viewportWidth) left = e.clientX - tooltipRect.width - 15;
      if (top + tooltipRect.height > viewportHeight) top = e.clientY - tooltipRect.height - 15;
      tooltip.style.left = left + 'px';
      tooltip.style.top = top + 'px';
    });
  });

  // Initialize drag and drop for customer project lists
  container.querySelectorAll('.projects-list').forEach(projectList => {
    initCustomerDragAndDrop(projectList);
  });

  // Add copy button event listeners for customer projects
  container.querySelectorAll('.customer-copy-btn').forEach(btn => {
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      const taskId = parseInt(btn.dataset.taskId);
      const source = btn.dataset.source;
      const type = btn.dataset.type;
      showCopyMenu(taskId, source, type);
    });
  });

  // Add complete button event listeners for customer projects
  container.querySelectorAll('.customer-complete-btn').forEach(btn => {
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      const projectId = parseInt(btn.dataset.projectId);
      const customerName = btn.dataset.customer;
      showConfirm(`Complete project #${projectId} from ${customerName}?`, () => {
        completeCustomerProject(projectId, customerName);
      });
    });
  });

  // Add delete button event listeners for customer projects
  container.querySelectorAll('.customer-delete-btn').forEach(btn => {
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      const projectId = parseInt(btn.dataset.projectId);
      const customerName = btn.dataset.customer;
      showConfirm(`Delete project #${projectId} from ${customerName}?`, () => {
        deleteCustomerProject(projectId, customerName);
      });
    });
  });
}

// Render delegation tasks
function renderDelegation(data, filter = 'all') {
  const container = document.getElementById('delegationList');
  if (!container || !data) return;

  let tasks = data.tasks || [];

  if (filter === 'inProgress') {
    tasks = tasks.filter(t => t.status === i18n.t('status_in_progress') || t.status === '进行中');
  } else if (filter === 'overdue') {
    tasks = tasks.filter(t => t.overdue);
  }

  if (tasks.length === 0) {
    container.innerHTML = `<div class="empty-state">${i18n.t('empty_delegation')}</div>`;
    return;
  }

  // Clear existing delegation tooltips
  document.querySelectorAll('.delegation-tooltip').forEach(t => t.remove());

  container.innerHTML = tasks.map(task => `
    <div class="delegation-card ${task.overdue ? 'overdue' : ''}" data-task-id="${task.id}">
      <div class="drag-handle delegation-drag-handle" draggable="true" title="Drag to reorder"></div>
      <button class="complete-btn delegation-complete-btn" data-task-id="${task.id}" title="Complete">✓</button>
      <button class="delete-btn delegation-delete-btn" data-task-id="${task.id}" title="Delete">×</button>
      <div class="delegation-header">
        <div class="delegation-title"><span class="task-id">#${task.id}</span><span class="delegation-title-text">${escapeHtml(task.title)}</span></div>
        <div class="delegation-status ${task.status === i18n.t('status_in_progress') || task.status === '进行中' ? 'inprogress' : 'todo'}">
          ${task.status}
        </div>
      </div>
      ${task.description ? `<div class="delegation-desc">${escapeHtml(task.description)}</div>` : ''}
      <div class="delegation-meta">
        <span class="delegation-assignee">${escapeHtml(task.assignee)}</span>
        ${task.deadline ? `<span class="delegation-deadline ${task.overdue ? 'overdue' : ''}">
          ${task.overdue ? i18n.t('deadline_overdue') : i18n.t('deadline_prefix')} ${escapeHtml(task.deadline)}
        </span>` : ''}
      </div>
    </div>
  `).join('');

  // Add hover tooltips for delegation cards
  container.querySelectorAll('.delegation-card').forEach((card, index) => {
    const task = tasks[index];
    if (!task) return;

    const tooltip = document.createElement('div');
    tooltip.className = 'task-tooltip delegation-tooltip';
    tooltip.innerHTML = `
      <div class="tooltip-header">
        <span class="tooltip-id">#${task.id}</span>
        ${task.overdue ? `<span class="tooltip-blocked">⚠️ ${i18n.t('deadline_overdue')}</span>` : ''}
      </div>
      <div class="tooltip-title">${escapeHtml(task.title)}</div>
      ${task.description ? `<div class="tooltip-section"><div class="tooltip-label">${i18n.t('description')}</div><div class="tooltip-desc">${escapeHtml(task.description)}</div></div>` : ''}
      <div class="tooltip-section">
        <div class="tooltip-label">${i18n.t('status_in_progress')}</div>
        <div class="tooltip-desc">${escapeHtml(task.status)}</div>
      </div>
      <div class="tooltip-section">
        <div class="tooltip-label">${i18n.t('assignee_prefix')}</div>
        <div class="tooltip-desc">${escapeHtml(task.assignee)}</div>
      </div>
      ${task.deadline ? `<div class="tooltip-section"><div class="tooltip-label">${i18n.t('deadline_prefix')}</div><div class="tooltip-desc ${task.overdue ? 'overdue' : ''}">${escapeHtml(task.deadline)}</div></div>` : ''}
      ${task.created ? `<div class="tooltip-meta"><span class="tooltip-label">${i18n.t('created_prefix')}</span> ${task.created}</div>` : ''}
    `;
    document.body.appendChild(tooltip);

    card.addEventListener('mouseenter', (e) => {
      tooltip.classList.add('show');
      const tooltipRect = tooltip.getBoundingClientRect();
      const viewportWidth = window.innerWidth;
      const viewportHeight = window.innerHeight;
      let left = e.clientX + 15;
      let top = e.clientY + 15;
      if (left + tooltipRect.width > viewportWidth) left = e.clientX - tooltipRect.width - 15;
      if (top + tooltipRect.height > viewportHeight) top = e.clientY - tooltipRect.height - 15;
      tooltip.style.left = left + 'px';
      tooltip.style.top = top + 'px';
    });

    card.addEventListener('mouseleave', () => tooltip.classList.remove('show'));

    card.addEventListener('mousemove', (e) => {
      const tooltipRect = tooltip.getBoundingClientRect();
      const viewportWidth = window.innerWidth;
      const viewportHeight = window.innerHeight;
      let left = e.clientX + 15;
      let top = e.clientY + 15;
      if (left + tooltipRect.width > viewportWidth) left = e.clientX - tooltipRect.width - 15;
      if (top + tooltipRect.height > viewportHeight) top = e.clientY - tooltipRect.height - 15;
      tooltip.style.left = left + 'px';
      tooltip.style.top = top + 'px';
    });
  });

  // Initialize drag and drop for delegation list
  initDelegationDragAndDrop(container);

  // Add complete button event listeners for delegation tasks
  container.querySelectorAll('.delegation-complete-btn').forEach(btn => {
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      const taskId = parseInt(btn.dataset.taskId);
      showConfirm(`Complete delegation task #${taskId}?`, () => {
        completeDelegationTask(taskId);
      });
    });
  });

  // Add delete button event listeners for delegation tasks
  container.querySelectorAll('.delegation-delete-btn').forEach(btn => {
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      const taskId = parseInt(btn.dataset.taskId);
      showConfirm(`Delete delegation task #${taskId}?`, () => {
        deleteDelegationTask(taskId);
      });
    });
  });
}

// Render maybe list
function renderMaybe(data) {
  const container = document.getElementById('maybeList');
  if (!container || !data) return;

  const tasks = data.tasks || [];

  if (tasks.length === 0) {
    container.innerHTML = `<div class="empty-state">${i18n.t('empty_maybe')}</div>`;
    return;
  }

  // Clear existing maybe tooltips
  document.querySelectorAll('.maybe-tooltip').forEach(t => t.remove());

  container.innerHTML = tasks.map(task => `
    <div class="maybe-card" data-task-id="${task.id}">
      <div class="drag-handle maybe-drag-handle" draggable="true" title="Drag to reorder"></div>
      <button class="complete-btn maybe-complete-btn" data-task-id="${task.id}" title="Complete">✓</button>
      <button class="move-btn maybe-move-btn" data-task-id="${task.id}" data-type="maybe" title="Move to Quadrant...">→</button>
      <button class="delete-btn maybe-delete-btn" data-task-id="${task.id}" title="Delete">×</button>
      <div class="maybe-header">
        <div class="maybe-title"><span class="task-id">#${task.id}</span><span class="maybe-title-text">${escapeHtml(task.title)}</span></div>
        ${task.category ? `<div class="maybe-category">${escapeHtml(task.category)}</div>` : ''}
      </div>
      ${task.description ? `<div class="maybe-desc">${escapeHtml(task.description)}</div>` : ''}
    </div>
  `).join('');

  // Add hover tooltips for maybe cards
  container.querySelectorAll('.maybe-card').forEach((card, index) => {
    const task = tasks[index];
    if (!task) return;

    const tooltip = document.createElement('div');
    tooltip.className = 'task-tooltip maybe-tooltip';
    tooltip.innerHTML = `
      <div class="tooltip-header">
        <span class="tooltip-id">#${task.id}</span>
        ${task.category ? `<span class="tooltip-priority">${escapeHtml(task.category)}</span>` : ''}
      </div>
      <div class="tooltip-title">${escapeHtml(task.title)}</div>
      ${task.description ? `<div class="tooltip-section"><div class="tooltip-label">${i18n.t('description')}</div><div class="tooltip-desc">${escapeHtml(task.description)}</div></div>` : ''}
      ${task.created ? `<div class="tooltip-meta"><span class="tooltip-label">${i18n.t('created_prefix')}</span> ${task.created}</div>` : ''}
    `;
    document.body.appendChild(tooltip);

    card.addEventListener('mouseenter', (e) => {
      tooltip.classList.add('show');
      const tooltipRect = tooltip.getBoundingClientRect();
      const viewportWidth = window.innerWidth;
      const viewportHeight = window.innerHeight;
      let left = e.clientX + 15;
      let top = e.clientY + 15;
      if (left + tooltipRect.width > viewportWidth) left = e.clientX - tooltipRect.width - 15;
      if (top + tooltipRect.height > viewportHeight) top = e.clientY - tooltipRect.height - 15;
      tooltip.style.left = left + 'px';
      tooltip.style.top = top + 'px';
    });

    card.addEventListener('mouseleave', () => tooltip.classList.remove('show'));

    card.addEventListener('mousemove', (e) => {
      const tooltipRect = tooltip.getBoundingClientRect();
      const viewportWidth = window.innerWidth;
      const viewportHeight = window.innerHeight;
      let left = e.clientX + 15;
      let top = e.clientY + 15;
      if (left + tooltipRect.width > viewportWidth) left = e.clientX - tooltipRect.width - 15;
      if (top + tooltipRect.height > viewportHeight) top = e.clientY - tooltipRect.height - 15;
      tooltip.style.left = left + 'px';
      tooltip.style.top = top + 'px';
    });
  });

  // Initialize drag and drop for maybe list
  initMaybeDragAndDrop(container);

  // Add complete button event listeners for maybe tasks
  container.querySelectorAll('.maybe-complete-btn').forEach(btn => {
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      const taskId = parseInt(btn.dataset.taskId);
      showConfirm(`Complete maybe task #${taskId}?`, () => {
        completeMaybeTask(taskId);
      });
    });
  });

  // Add move button event listeners for maybe tasks
  container.querySelectorAll('.maybe-move-btn').forEach(btn => {
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      const taskId = parseInt(btn.dataset.taskId);
      const type = btn.dataset.type;
      showMoveMenu(taskId, type);
    });
  });

  // Add delete button event listeners for maybe tasks
  container.querySelectorAll('.maybe-delete-btn').forEach(btn => {
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      const taskId = parseInt(btn.dataset.taskId);
      showConfirm(`Delete maybe task #${taskId}?`, () => {
        deleteMaybeTask(taskId);
      });
    });
  });
}

// Update timestamp
function updateTimestamp() {
  const el = document.getElementById('lastUpdate');
  if (el && currentData?.timestamp) {
    const date = new Date(currentData.timestamp);
    // Use locale based on current language
    const locale = i18n.getLanguage() === 'zh-CN' ? 'zh-CN' : 'en-US';
    el.textContent = date.toLocaleString(locale);
  }
}

// Escape HTML to prevent XSS
function escapeHtml(text) {
  if (!text) return '';
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

// Refresh button handler (manual refresh)
function refreshData() {
  fetchInitialData();
}

// Export for debugging
window.taskDashboard = {
  refresh: refreshData,
  getData: () => currentData,
  getWebSocket: () => ws
};

// Delete task from tasks.md
async function completeTask(taskId, quadrant) {
  try {
    console.log('[Complete] Task:', taskId, 'from', quadrant);
    
    const response = await fetch('/api/tasks/complete', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ taskId, quadrant })
    });
    
    if (!response.ok) {
      throw new Error('Failed to complete task: ' + response.statusText);
    }
    
    const result = await response.json();
    console.log('[Complete] Success:', result);
    
    await fetchInitialData();
    
  } catch (error) {
    console.error('[Complete] Error:', error);
    alert('Failed to complete task: ' + error.message);
  }
}

async function deleteTask(taskId, quadrant) {
  try {
    console.log('[Delete] Task:', taskId, 'from', quadrant);
    
    const response = await fetch('/api/tasks/delete', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ taskId, quadrant })
    });
    
    if (!response.ok) {
      throw new Error('Failed to delete task: ' + response.statusText);
    }
    
    const result = await response.json();
    console.log('[Delete] Success:', result);
    
    await fetchInitialData();
    
  } catch (error) {
    console.error('[Delete] Error:', error);
    alert('Failed to delete task: ' + error.message);
  }
}

// Complete customer project
async function completeCustomerProject(projectId, customerName) {
  try {
    console.log('[Complete] Customer project:', projectId, 'from', customerName);
    
    const response = await fetch('/api/customer-projects/complete', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ projectId, customerName })
    });
    
    if (!response.ok) {
      throw new Error('Failed to complete project: ' + response.statusText);
    }
    
    const result = await response.json();
    console.log('[Complete] Success:', result);
    
    await fetchInitialData();
    
  } catch (error) {
    console.error('[Complete] Error:', error);
    alert('Failed to complete project: ' + error.message);
  }
}

// Delete customer project
async function deleteCustomerProject(projectId, customerName) {
  try {
    console.log('[Delete] Customer project:', projectId, 'from', customerName);
    
    const response = await fetch('/api/customer-projects/delete', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ projectId, customerName })
    });
    
    if (!response.ok) {
      throw new Error('Failed to delete project: ' + response.statusText);
    }
    
    const result = await response.json();
    console.log('[Delete] Success:', result);
    
    await fetchInitialData();
    
  } catch (error) {
    console.error('[Delete] Error:', error);
    alert('Failed to delete project: ' + error.message);
  }
}

// Complete delegation task
async function completeDelegationTask(taskId) {
  try {
    console.log('[Complete] Delegation task:', taskId);
    
    const response = await fetch('/api/delegation/complete', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ taskId })
    });
    
    if (!response.ok) {
      throw new Error('Failed to complete task: ' + response.statusText);
    }
    
    const result = await response.json();
    console.log('[Complete] Success:', result);
    
    await fetchInitialData();
    
  } catch (error) {
    console.error('[Complete] Error:', error);
    alert('Failed to complete task: ' + error.message);
  }
}

// Delete delegation task
async function deleteDelegationTask(taskId) {
  try {
    console.log('[Delete] Delegation task:', taskId);
    
    const response = await fetch('/api/delegation/delete', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ taskId })
    });
    
    if (!response.ok) {
      throw new Error('Failed to delete task: ' + response.statusText);
    }
    
    const result = await response.json();
    console.log('[Delete] Success:', result);
    
    await fetchInitialData();
    
  } catch (error) {
    console.error('[Delete] Error:', error);
    alert('Failed to delete task: ' + error.message);
  }
}

// Complete maybe task
async function completeMaybeTask(taskId) {
  try {
    console.log('[Complete] Maybe task:', taskId);
    
    const response = await fetch('/api/maybe/complete', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ taskId })
    });
    
    if (!response.ok) {
      throw new Error('Failed to complete task: ' + response.statusText);
    }
    
    const result = await response.json();
    console.log('[Complete] Success:', result);
    
    await fetchInitialData();
    
  } catch (error) {
    console.error('[Complete] Error:', error);
    alert('Failed to complete task: ' + error.message);
  }
}

// Delete maybe task
async function deleteMaybeTask(taskId) {
  try {
    console.log('[Delete] Maybe task:', taskId);
    
    const response = await fetch('/api/maybe/delete', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ taskId })
    });
    
    if (!response.ok) {
      throw new Error('Failed to delete task: ' + response.statusText);
    }
    
    const result = await response.json();
    console.log('[Delete] Success:', result);
    
    await fetchInitialData();
    
  } catch (error) {
    console.error('[Delete] Error:', error);
    alert('Failed to delete task: ' + error.message);
  }
}

// Copy Menu Functions
let currentCopyTaskId = null;
let currentCopySource = null;
let currentCopyType = null;

function showCopyMenu(taskId, source, type) {
  currentCopyTaskId = taskId;
  currentCopySource = source;
  currentCopyType = type;
  
  // Remove existing menu
  const existingMenu = document.getElementById('copyMenuOverlay');
  if (existingMenu) {
    existingMenu.remove();
  }
  
  const overlay = document.createElement('div');
  overlay.id = 'copyMenuOverlay';
  overlay.className = 'copy-menu-overlay';
  
  const isCustomerProject = type === 'customer';
  const isQuadrantTask = type === 'quadrant';
  const title = isCustomerProject ? i18n.t('copy_project_to') || 'Copy Project to...' : i18n.t('copy_task_to') || 'Copy Task to...';
  
  // Build customer options if copying from quadrant
  let customerOptions = '';
  if (isQuadrantTask && currentData?.customerProjects?.customers) {
    customerOptions = currentData.customerProjects.customers.map(c => 
      `<button class="copy-menu-btn customer" data-target="customer" data-customer="${escapeHtml(c.name)}">🏢 ${escapeHtml(c.name)}</button>`
    ).join('');
  }
  
  // Build delegation option for quadrant tasks
  let delegationOption = '';
  if (isQuadrantTask) {
    delegationOption = `
      <div class="copy-menu-section">
        <div class="copy-menu-label">${i18n.t('delegation') || 'Delegation'}</div>
        <div class="copy-menu-options">
          <button class="copy-menu-btn delegation" data-target="delegation">👑 ${i18n.t('delegation_list') || 'Delegation List'}</button>
        </div>
      </div>
    `;
  }
  
  overlay.innerHTML = `
    <div class="copy-menu">
      <div class="copy-menu-header">${title}</div>
      <div class="copy-menu-section">
        <div class="copy-menu-label">${i18n.t('quadrants') || 'Four Quadrants'}</div>
        <div class="copy-menu-options">
          <button class="copy-menu-btn q1" data-target="Q1">🔥 Q1</button>
          <button class="copy-menu-btn q2" data-target="Q2">💼 Q2</button>
          <button class="copy-menu-btn q3" data-target="Q3">⚡ Q3</button>
          <button class="copy-menu-btn q4" data-target="Q4">🧘 Q4</button>
        </div>
      </div>
      ${isQuadrantTask && customerOptions ? `
      <div class="copy-menu-section">
        <div class="copy-menu-label">${i18n.t('customer_projects') || 'Customer Projects'}</div>
        <div class="copy-menu-options">
          ${customerOptions}
        </div>
      </div>
      ` : ''}
      ${delegationOption}
      ${isCustomerProject ? `
      <div class="copy-menu-section">
        <div class="copy-menu-label">${i18n.t('delegation') || 'Delegation'}</div>
        <div class="copy-menu-options">
          <button class="copy-menu-btn delegation" data-target="delegation">👑 ${i18n.t('delegation_list') || 'Delegation List'}</button>
        </div>
      </div>
      ` : ''}
      <button class="copy-menu-cancel">${i18n.t('cancel') || 'Cancel'}</button>
    </div>
  `;
  
  document.body.appendChild(overlay);
  
  // Show menu with animation
  requestAnimationFrame(() => {
    overlay.classList.add('show');
  });
  
  // Add event listeners
  overlay.querySelectorAll('.copy-menu-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      const target = btn.dataset.target;
      const customerName = btn.dataset.customer;
      executeCopy(target, customerName);
      hideCopyMenu();
    });
  });
  
  overlay.querySelector('.copy-menu-cancel').addEventListener('click', hideCopyMenu);
  
  // Close on overlay click
  overlay.addEventListener('click', (e) => {
    if (e.target === overlay) {
      hideCopyMenu();
    }
  });
  
  // Close on Escape key
  document.addEventListener('keydown', handleCopyMenuKeydown);
}

function hideCopyMenu() {
  const overlay = document.getElementById('copyMenuOverlay');
  if (overlay) {
    overlay.classList.remove('show');
    setTimeout(() => overlay.remove(), 200);
  }
  document.removeEventListener('keydown', handleCopyMenuKeydown);
  currentCopyTaskId = null;
  currentCopySource = null;
  currentCopyType = null;
}

function handleCopyMenuKeydown(e) {
  if (e.key === 'Escape') {
    hideCopyMenu();
  }
}

async function executeCopy(target, customerName = null) {
  if (!currentCopyTaskId || !currentCopySource) return;
  
  console.log('[Copy]', currentCopyType, currentCopyTaskId, 'from', currentCopySource, 'to', target, customerName ? `customer: ${customerName}` : '');
  
  try {
    // Handle copy to delegation list
    if (target === 'delegation' && currentCopyType === 'quadrant') {
      const response = await fetch('/api/tasks/copy-to-delegation', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ 
          taskId: currentCopyTaskId, 
          sourceQuadrant: currentCopySource
        })
      });
      
      if (!response.ok) {
        throw new Error('Failed to copy to delegation: ' + response.statusText);
      }
      
      const result = await response.json();
      console.log('[Copy to Delegation] Success:', result);
      await fetchInitialData();
      return;
    }
    
    // Handle copy to customer project
    if (target === 'customer' && customerName) {
      const response = await fetch('/api/tasks/copy-to-customer', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ 
          taskId: currentCopyTaskId, 
          sourceQuadrant: currentCopySource, 
          targetCustomer: customerName 
        })
      });
      
      if (!response.ok) {
        throw new Error('Failed to copy: ' + response.statusText);
      }
      
      const result = await response.json();
      console.log('[Copy] Success:', result);
      await fetchInitialData();
      return;
    }
    
    const endpoint = currentCopyType === 'customer' 
      ? '/api/customer-projects/copy' 
      : '/api/tasks/copy';
    
    const body = currentCopyType === 'customer'
      ? { projectId: currentCopyTaskId, sourceCustomer: currentCopySource, target }
      : { taskId: currentCopyTaskId, sourceQuadrant: currentCopySource, target };
    
    const response = await fetch(endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body)
    });
    
    if (!response.ok) {
      throw new Error('Failed to copy: ' + response.statusText);
    }
    
    const result = await response.json();
    console.log('[Copy] Success:', result);
    
    await fetchInitialData();
    
  } catch (error) {
    console.error('[Copy] Error:', error);
    alert('Failed to copy: ' + error.message);
  }
}

// Move Menu Functions (for Maybe List)
let currentMoveTaskId = null;
let currentMoveType = null;

function showMoveMenu(taskId, type) {
  currentMoveTaskId = taskId;
  currentMoveType = type;
  
  // Remove existing menu
  const existingMenu = document.getElementById('moveMenuOverlay');
  if (existingMenu) {
    existingMenu.remove();
  }
  
  const overlay = document.createElement('div');
  overlay.id = 'moveMenuOverlay';
  overlay.className = 'copy-menu-overlay';
  
  const title = i18n.t('move_task_to') || 'Move Task to Quadrant...';
  
  overlay.innerHTML = `
    <div class="copy-menu">
      <div class="copy-menu-header">${title}</div>
      <div class="copy-menu-section">
        <div class="copy-menu-label">${i18n.t('quadrants') || 'Four Quadrants'}</div>
        <div class="copy-menu-options">
          <button class="copy-menu-btn q1" data-target="Q1">🔥 Q1</button>
          <button class="copy-menu-btn q2" data-target="Q2">💼 Q2</button>
          <button class="copy-menu-btn q3" data-target="Q3">⚡ Q3</button>
          <button class="copy-menu-btn q4" data-target="Q4">🧘 Q4</button>
        </div>
      </div>
      <button class="copy-menu-cancel">${i18n.t('cancel') || 'Cancel'}</button>
    </div>
  `;
  
  document.body.appendChild(overlay);
  
  // Show menu with animation
  requestAnimationFrame(() => {
    overlay.classList.add('show');
  });
  
  // Add event listeners
  overlay.querySelectorAll('.copy-menu-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      const target = btn.dataset.target;
      executeMove(target);
      hideMoveMenu();
    });
  });
  
  overlay.querySelector('.copy-menu-cancel').addEventListener('click', hideMoveMenu);
  
  // Close on overlay click
  overlay.addEventListener('click', (e) => {
    if (e.target === overlay) {
      hideMoveMenu();
    }
  });
  
  // Close on Escape key
  document.addEventListener('keydown', handleMoveMenuKeydown);
}

function hideMoveMenu() {
  const overlay = document.getElementById('moveMenuOverlay');
  if (overlay) {
    overlay.classList.remove('show');
    setTimeout(() => overlay.remove(), 200);
  }
  document.removeEventListener('keydown', handleMoveMenuKeydown);
  currentMoveTaskId = null;
  currentMoveType = null;
}

function handleMoveMenuKeydown(e) {
  if (e.key === 'Escape') {
    hideMoveMenu();
  }
}

async function executeMove(target) {
  if (!currentMoveTaskId) return;
  
  console.log('[Move]', currentMoveType, currentMoveTaskId, 'to', target);
  
  try {
    const response = await fetch('/api/maybe/move', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ 
        taskId: currentMoveTaskId, 
        targetQuadrant: target 
      })
    });
    
    if (!response.ok) {
      throw new Error('Failed to move: ' + response.statusText);
    }
    
    const result = await response.json();
    console.log('[Move] Success:', result);
    
    await fetchInitialData();
    
  } catch (error) {
    console.error('[Move] Error:', error);
    alert('Failed to move: ' + error.message);
  }
}

// Move task from quadrant to Maybe List
let currentMoveToTaskId = null;
let currentMoveToSource = null;

function showMoveToMenu(taskId, source) {
  currentMoveToTaskId = taskId;
  currentMoveToSource = source;
  
  // Remove existing menu
  const existingMenu = document.getElementById('moveToMenuOverlay');
  if (existingMenu) {
    existingMenu.remove();
  }
  
  const overlay = document.createElement('div');
  overlay.id = 'moveToMenuOverlay';
  overlay.className = 'copy-menu-overlay';
  
  const title = i18n.t('move_task_to') || 'Move Task to...';
  
  overlay.innerHTML = `
    <div class="copy-menu">
      <div class="copy-menu-header">${title}</div>
      <div class="copy-menu-section">
        <div class="copy-menu-label">${i18n.t('target_list') || 'Target List'}</div>
        <div class="copy-menu-options">
          <button class="copy-menu-btn maybe" data-target="maybe">🌱 ${i18n.t('maybe_list') || 'Maybe List'}</button>
          <button class="copy-menu-btn delegation" data-target="delegation">👑 ${i18n.t('delegation_list') || 'Delegation List'}</button>
        </div>
      </div>
      <button class="copy-menu-cancel">${i18n.t('cancel') || 'Cancel'}</button>
    </div>
  `;
  
  document.body.appendChild(overlay);
  
  // Show menu with animation
  requestAnimationFrame(() => {
    overlay.classList.add('show');
  });
  
  // Add event listeners
  overlay.querySelectorAll('.copy-menu-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      const target = btn.dataset.target;
      executeMoveTo(target);
      hideMoveToMenu();
    });
  });
  
  overlay.querySelector('.copy-menu-cancel').addEventListener('click', hideMoveToMenu);
  
  // Close on overlay click
  overlay.addEventListener('click', (e) => {
    if (e.target === overlay) {
      hideMoveToMenu();
    }
  });
  
  // Close on Escape key
  document.addEventListener('keydown', handleMoveToMenuKeydown);
}

function hideMoveToMenu() {
  const overlay = document.getElementById('moveToMenuOverlay');
  if (overlay) {
    overlay.classList.remove('show');
    setTimeout(() => overlay.remove(), 200);
  }
  document.removeEventListener('keydown', handleMoveToMenuKeydown);
  currentMoveToTaskId = null;
  currentMoveToSource = null;
}

function handleMoveToMenuKeydown(e) {
  if (e.key === 'Escape') {
    hideMoveToMenu();
  }
}

async function executeMoveTo(target) {
  if (!currentMoveToTaskId || !currentMoveToSource) return;
  
  if (target === 'maybe') {
    await moveTaskToMaybe(currentMoveToTaskId, currentMoveToSource);
  } else if (target === 'delegation') {
    await moveTaskToDelegation(currentMoveToTaskId, currentMoveToSource);
  }
}

async function moveTaskToMaybe(taskId, sourceQuadrant) {
  try {
    console.log('[Move to Maybe] Task:', taskId, 'from', sourceQuadrant);
    
    const response = await fetch('/api/tasks/move-to-maybe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ taskId, sourceQuadrant })
    });
    
    if (!response.ok) {
      throw new Error('Failed to move task: ' + response.statusText);
    }
    
    const result = await response.json();
    console.log('[Move to Maybe] Success:', result);
    
    await fetchInitialData();
    
  } catch (error) {
    console.error('[Move to Maybe] Error:', error);
    alert('Failed to move: ' + error.message);
  }
}

async function moveTaskToDelegation(taskId, sourceQuadrant) {
  try {
    console.log('[Move to Delegation] Task:', taskId, 'from', sourceQuadrant);
    
    const response = await fetch('/api/tasks/move-to-delegation', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ taskId, sourceQuadrant })
    });
    
    if (!response.ok) {
      throw new Error('Failed to move task: ' + response.statusText);
    }
    
    const result = await response.json();
    console.log('[Move to Delegation] Success:', result);
    
    await fetchInitialData();
    
  } catch (error) {
    console.error('[Move to Delegation] Error:', error);
    alert('Failed to move: ' + error.message);
  }
}

// Delegation List Drag and Drop
let draggedDelegationTask = null;

function initDelegationDragAndDrop(container) {
  container.addEventListener('dragover', handleDelegationDragOver);
  container.addEventListener('dragenter', handleDelegationDragEnter);
  container.addEventListener('dragleave', handleDelegationDragLeave);
  container.addEventListener('drop', handleDelegationDrop);
  
  container.querySelectorAll('.delegation-drag-handle').forEach(handle => {
    handle.draggable = true;
    handle.addEventListener('dragstart', handleDelegationDragStart);
    handle.addEventListener('dragend', handleDelegationDragEnd);
  });
}

function handleDelegationDragEnter(e) {
  e.preventDefault();
  // Only highlight if dragging a delegation task
  if (!draggedDelegationTask) return;
  const container = e.currentTarget;
  container.classList.add('drag-over');
}

function handleDelegationDragLeave(e) {
  const container = e.currentTarget;
  // Only remove if leaving the container entirely (not entering a child)
  if (!container.contains(e.relatedTarget)) {
    container.classList.remove('drag-over');
  }
}

function handleDelegationDragStart(e) {
  const card = e.target.closest('.delegation-card');
  if (!card) return;
  
  draggedDelegationTask = {
    id: parseInt(card.dataset.taskId),
    element: card
  };
  
  card.classList.add('dragging');
  e.dataTransfer.effectAllowed = 'move';
  console.log('[Delegation Drag] Started:', draggedDelegationTask);
}

function handleDelegationDragEnd(e) {
  const card = e.target.closest('.delegation-card');
  if (card) {
    card.classList.remove('dragging');
  }
  // Clean up all highlights
  document.querySelectorAll('.delegation-card').forEach(c => {
    c.classList.remove('drag-over');
  });
  document.querySelectorAll('.delegation-list').forEach(el => {
    el.classList.remove('drag-over');
  });
  draggedDelegationTask = null;
}

function handleDelegationDragOver(e) {
  e.preventDefault();
  e.dataTransfer.dropEffect = 'move';

  if (!draggedDelegationTask) return;

  const container = e.currentTarget;
  const afterElement = getDragAfterElement(container, e.clientY, '.delegation-card', e.clientX);

  // Clear previous highlights
  container.querySelectorAll('.delegation-card').forEach(card => {
    card.classList.remove('drag-over');
  });

  // Highlight the target position
  if (afterElement) {
    afterElement.classList.add('drag-over');
  } else {
    // If no afterElement, highlight the last card or indicate append
    const cards = container.querySelectorAll('.delegation-card:not(.dragging)');
    if (cards.length > 0) {
      cards[cards.length - 1].classList.add('drag-over');
    }
  }
}

async function handleDelegationDrop(e) {
  e.preventDefault();

  if (!draggedDelegationTask) return;

  const container = e.currentTarget;
  const taskId = draggedDelegationTask.id;

  // Find insert position
  const targetElement = getDragAfterElement(container, e.clientY, '.delegation-card', e.clientX);
  let insertIndex = -1;

  if (targetElement) {
    const allCards = [...container.querySelectorAll('.delegation-card')];
    const targetTaskId = parseInt(targetElement.dataset.taskId);
    const targetIdx = allCards.findIndex(c => parseInt(c.dataset.taskId) === targetTaskId);
    
    // Insert at the target card's position
    // This will push the target card and all after it one position back
    insertIndex = targetIdx;
  }

  // Clear highlights
  container.querySelectorAll('.delegation-card').forEach(card => {
    card.classList.remove('drag-over');
  });
  container.classList.remove('drag-over');

  try {
    const response = await fetch('/api/delegation/reorder', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ taskId, insertIndex })
    });

    if (!response.ok) {
      throw new Error('Failed to reorder: ' + response.statusText);
    }

    await fetchInitialData();

  } catch (error) {
    console.error('[Delegation Drop] Error:', error);
    await fetchInitialData();
  }
}

// Maybe List Drag and Drop
let draggedMaybeTask = null;

function initMaybeDragAndDrop(container) {
  container.addEventListener('dragover', handleMaybeDragOver);
  container.addEventListener('dragenter', handleMaybeDragEnter);
  container.addEventListener('dragleave', handleMaybeDragLeave);
  container.addEventListener('drop', handleMaybeDrop);
  
  container.querySelectorAll('.maybe-drag-handle').forEach(handle => {
    handle.draggable = true;
    handle.addEventListener('dragstart', handleMaybeDragStart);
    handle.addEventListener('dragend', handleMaybeDragEnd);
  });
}

function handleMaybeDragEnter(e) {
  e.preventDefault();
  // Only highlight if dragging a maybe task
  if (!draggedMaybeTask) return;
  const container = e.currentTarget;
  container.classList.add('drag-over');
}

function handleMaybeDragLeave(e) {
  const container = e.currentTarget;
  // Only remove if leaving the container entirely (not entering a child)
  if (!container.contains(e.relatedTarget)) {
    container.classList.remove('drag-over');
  }
}

function handleMaybeDragStart(e) {
  const card = e.target.closest('.maybe-card');
  if (!card) return;
  
  draggedMaybeTask = {
    id: parseInt(card.dataset.taskId),
    element: card
  };
  
  card.classList.add('dragging');
  e.dataTransfer.effectAllowed = 'move';
  console.log('[Maybe Drag] Started:', draggedMaybeTask);
}

function handleMaybeDragEnd(e) {
  const card = e.target.closest('.maybe-card');
  if (card) {
    card.classList.remove('dragging');
  }
  // Clean up all highlights
  document.querySelectorAll('.maybe-card').forEach(c => {
    c.classList.remove('drag-over');
  });
  document.querySelectorAll('.maybe-list').forEach(el => {
    el.classList.remove('drag-over');
  });
  draggedMaybeTask = null;
}

function handleMaybeDragOver(e) {
  e.preventDefault();
  e.dataTransfer.dropEffect = 'move';

  if (!draggedMaybeTask) return;

  const container = e.currentTarget;
  const afterElement = getDragAfterElement(container, e.clientY, '.maybe-card', e.clientX);

  // Clear previous highlights
  container.querySelectorAll('.maybe-card').forEach(card => {
    card.classList.remove('drag-over');
  });

  // Highlight the target position
  if (afterElement) {
    afterElement.classList.add('drag-over');
  } else {
    // If no afterElement, highlight the last card or indicate append
    const cards = container.querySelectorAll('.maybe-card:not(.dragging)');
    if (cards.length > 0) {
      cards[cards.length - 1].classList.add('drag-over');
    }
  }
}

async function handleMaybeDrop(e) {
  e.preventDefault();

  if (!draggedMaybeTask) return;

  const container = e.currentTarget;
  const taskId = draggedMaybeTask.id;

  // Find insert position
  // UI highlights a card to indicate "place dragged item HERE"
  const targetElement = getDragAfterElement(container, e.clientY, '.maybe-card', e.clientX);
  let insertIndex = -1; // -1 means append to end

  if (targetElement) {
    const allCards = [...container.querySelectorAll('.maybe-card')];
    const targetTaskId = parseInt(targetElement.dataset.taskId);
    const targetIdx = allCards.findIndex(c => parseInt(c.dataset.taskId) === targetTaskId);
    
    // Insert at the target card's position
    insertIndex = targetIdx;
  }

  // Clear highlights
  container.querySelectorAll('.maybe-card').forEach(card => {
    card.classList.remove('drag-over');
  });
  container.classList.remove('drag-over');

  console.log('[Maybe Drop] Task:', taskId, 'at index', insertIndex);

  try {
    const response = await fetch('/api/maybe/reorder', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ taskId, insertIndex })
    });

    if (!response.ok) {
      throw new Error('Failed to reorder: ' + response.statusText);
    }

    await fetchInitialData();

  } catch (error) {
    console.error('[Maybe Drop] Error:', error);
    await fetchInitialData();
  }
}

// Custom Confirm Function
function showConfirm(message, onConfirm) {
  // Prevent multiple dialogs
  if (document.getElementById('customConfirmOverlay')) {
    return;
  }

  const overlay = document.createElement('div');
  overlay.id = 'customConfirmOverlay';
  overlay.className = 'custom-alert-overlay';

  overlay.innerHTML = `
    <div class="custom-alert-modal">
      <div class="custom-alert-message">${escapeHtml(message)}</div>
      <div class="custom-confirm-actions">
        <button class="custom-alert-btn custom-confirm-cancel">Cancel</button>
        <button class="custom-alert-btn custom-confirm-ok">OK</button>
      </div>
    </div>
  `;

  document.body.appendChild(overlay);

  const btnOk = overlay.querySelector('.custom-confirm-ok');
  const btnCancel = overlay.querySelector('.custom-confirm-cancel');
  
  let isClosing = false;
  
  const closeConfirm = () => {
    if (isClosing) return;
    isClosing = true;
    
    document.removeEventListener('keydown', handleKeydown);
    overlay.classList.remove('show');
    
    setTimeout(() => {
      if (overlay.parentNode) {
        overlay.remove();
      }
    }, 200);
  };

  const handleKeydown = (e) => {
    if (isClosing) return;
    
    if (e.key === 'Escape') {
      e.preventDefault();
      e.stopPropagation();
      closeConfirm();
    } else if (e.key === 'Enter') {
      e.preventDefault();
      e.stopPropagation();
      closeConfirm();
      if (onConfirm) onConfirm();
    }
  };

  btnOk.addEventListener('click', (e) => {
    e.preventDefault();
    e.stopPropagation();
    closeConfirm();
    if (onConfirm) onConfirm();
  });

  btnCancel.addEventListener('click', (e) => {
    e.preventDefault();
    e.stopPropagation();
    closeConfirm();
  });
  
  overlay.addEventListener('click', (e) => {
    if (e.target === overlay) {
      closeConfirm();
    }
  });

  document.addEventListener('keydown', handleKeydown);

  // Trigger animation
  requestAnimationFrame(() => {
    overlay.classList.add('show');
    // Remove focus to prevent Enter key double trigger
    btnCancel.blur();
    btnOk.blur();
  });
}


path: dashboard/public/favicon.svg
content: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
  <defs>
    <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" style="stop-color:#1e293b"/>
      <stop offset="100%" style="stop-color:#0f172a"/>
    </linearGradient>
    <linearGradient id="q1" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" style="stop-color:#ef4444"/>
      <stop offset="100%" style="stop-color:#dc2626"/>
    </linearGradient>
    <linearGradient id="q2" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" style="stop-color:#3b82f6"/>
      <stop offset="100%" style="stop-color:#2563eb"/>
    </linearGradient>
    <linearGradient id="q3" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" style="stop-color:#f59e0b"/>
      <stop offset="100%" style="stop-color:#d97706"/>
    </linearGradient>
    <linearGradient id="q4" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" style="stop-color:#10b981"/>
      <stop offset="100%" style="stop-color:#059669"/>
    </linearGradient>
  </defs>
  
  <!-- Background -->
  <rect x="5" y="5" width="90" height="90" rx="18" fill="url(#bg)"/>
  
  <!-- Q1: Top Right - Red (Urgent & Important) -->
  <path d="M52 10 L85 10 Q90 10 90 15 L90 48 L52 48 Z" fill="url(#q1)" opacity="0.95"/>
  
  <!-- Q2: Top Left - Blue (Important, Not Urgent) -->
  <path d="M10 15 Q10 10 15 10 L48 10 L48 48 L10 48 L10 15 Z" fill="url(#q2)" opacity="0.95"/>
  
  <!-- Q3: Bottom Right - Orange (Urgent, Not Important) -->
  <path d="M52 52 L90 52 L90 85 Q90 90 85 90 L52 90 Z" fill="url(#q3)" opacity="0.95"/>
  
  <!-- Q4: Bottom Left - Green (Not Urgent, Not Important) -->
  <path d="M15 90 Q10 90 10 85 L10 52 L48 52 L48 90 L15 90 Z" fill="url(#q4)" opacity="0.95"/>
  
  <!-- Center Cross -->
  <rect x="48" y="10" width="4" height="80" fill="#1e293b" opacity="0.3"/>
  <rect x="10" y="48" width="80" height="4" fill="#1e293b" opacity="0.3"/>
  
  <!-- Letter E in center -->
  <text x="50" y="62" font-family="system-ui, -apple-system, sans-serif" font-size="32" font-weight="bold" fill="white" text-anchor="middle">E</text>
</svg>

path: dashboard/public/i18n.js
content: /**
 * Internationalization (i18n) Module
 * Bilingual support for Chinese (zh-CN) and English (en)
 */

const i18n = {
  // Current language
  currentLang: localStorage.getItem('dashboard-lang') || 'en',

  // Translation dictionary
  translations: {
    'en': {
      // Header
      'title': '📊 Eisenhower Task Dashboard',
      'status_connecting': 'Connecting...',
      'status_live': 'Live',
      'status_offline': 'Offline',

      // Stats bar
      'q1_label': '🔥 Q1 Urgent & Important',
      'q2_label': '💼 Q2 Important Not Urgent',
      'q3_label': '⚡ Q3 Urgent Not Important',
      'q4_label': '🧘 Q4 Not Urgent & Important',
      'customer_label': '🏢 Customer Projects',
      'delegation_label': '👑 Delegation Tasks',
      'maybe_label': '🌱 Maybe List',

      // Navigation tabs
      'tab_matrix': 'Eisenhower Matrix',
      'tab_customers': 'Customer Projects',
      'tab_delegation': 'Delegation',
      'tab_maybe': 'Maybe List',

      // Quadrant section titles
      'q1_title': '🔥 Q1: Urgent & Important',
      'q1_subtitle': 'Do First',
      'q2_title': '💼 Q2: Important Not Urgent',
      'q2_subtitle': 'Schedule',
      'q3_title': '⚡ Q3: Not Important Urgent',
      'q3_subtitle': 'Batch / Delegate',
      'q4_title': '🧘 Q4: Not Urgent & Important',
      'q4_subtitle': 'Delete / Postpone',

      // Customer section
      'customer_title': '🏢 Customer Project List',
      'filter_all': 'All',
      'filter_active': 'Active',
      'filter_blocked': 'Blocked',

      // Delegation section
      'delegation_title': '👑 Delegation Task List',
      'filter_inprogress': 'In Progress',
      'filter_overdue': 'Overdue',
      'assignee_prefix': 'Assignee:',
      'deadline_prefix': 'Due:',
      'deadline_overdue': '⚠️ Overdue:',

      // Maybe section
      'maybe_title': '🌱 Maybe List',
      'maybe_desc': 'Ideas and inspirations for future consideration, not yet in the matrix',

      // Footer
      'last_updated': 'Last updated:',

      // Empty states
      'empty_tasks': 'No tasks',
      'empty_customer': 'No customer projects',
      'empty_delegation': 'No delegation tasks',
      'empty_maybe': 'No future plans',
      'empty_filtered': 'No matching items',

      // Task meta
      'created_prefix': 'Created:',
      'updated_prefix': 'Updated:',
      'reviewed_prefix': 'Reviewed:',
      'blocked_badge': '🚫 Blocked',
      'type_prefix': 'Type:',
      'priority_prefix': 'Priority:',
      'description': 'Description',
      'subtasks': 'Subtasks',
      'tags': 'Tags',
      'assignee_prefix': 'Assignee:',

      // Language switcher
      'language': 'Language',
      'lang_en': 'English',
      'lang_zh': '中文',

      // Status labels
      'status_active': 'Active',
      'status_blocked': 'Blocked',
      'status_pending': 'Pending',
      'status_in_progress': 'In Progress',
      'status_todo': 'To Do',

      // Drag and drop
      'drag_to_reorder': 'Drag to reorder',

      // Copy menu
      'copy_task_to': 'Copy Task to...',
      'copy_project_to': 'Copy Project to...',
      'quadrants': 'Four Quadrants',
      'delegation': 'Delegation',
      'delegation_list': 'Delegation List',
      'customer_projects': 'Customer Projects',
      'cancel': 'Cancel',

      // Move menu
      'move_task_to': 'Move Task to...',
      'target_list': 'Target List',
      'maybe_list': 'Maybe List'
    },
    'zh-CN': {
      // Header
      'title': '📊 Eisenhower Task Dashboard',
      'status_connecting': '连接中...',
      'status_live': '实时连接',
      'status_offline': '离线',

      // Stats bar
      'q1_label': '🔥 Q1 重要+紧急',
      'q2_label': '💼 Q2 重要不紧急',
      'q3_label': '⚡ Q3 不重要紧急',
      'q4_label': '🧘 Q4 不重要不紧急',
      'customer_label': '🏢 客户项目',
      'delegation_label': '👑 委派任务',
      'maybe_label': '🌱 未来清单',

      // Navigation tabs
      'tab_matrix': '四象限矩阵',
      'tab_customers': '客户项目',
      'tab_delegation': '委派任务',
      'tab_maybe': '未来清单',

      // Quadrant section titles
      'q1_title': '🔥 Q1: 重要+紧急',
      'q1_subtitle': '立即执行',
      'q2_title': '💼 Q2: 重要不紧急',
      'q2_subtitle': '计划执行',
      'q3_title': '⚡ Q3: 不重要紧急',
      'q3_subtitle': '批量/委派',
      'q4_title': '🧘 Q4: 不重要不紧急',
      'q4_subtitle': '推迟/删除',

      // Customer section
      'customer_title': '🏢 客户项目清单',
      'filter_all': '全部',
      'filter_active': '进行中',
      'filter_blocked': '阻塞中',

      // Delegation section
      'delegation_title': '👑 委派任务清单',
      'filter_inprogress': '进行中',
      'filter_overdue': '已逾期',
      'assignee_prefix': '责任人:',
      'deadline_prefix': '截止:',
      'deadline_overdue': '⚠️ 已逾期:',

      // Maybe section
      'maybe_title': '🌱 未来清单 (Maybe List)',
      'maybe_desc': '可能以后做的想法和灵感，暂不放入四象限',

      // Footer
      'last_updated': '最后更新:',

      // Empty states
      'empty_tasks': '暂无任务',
      'empty_customer': '暂无客户项目',
      'empty_delegation': '暂无委派任务',
      'empty_maybe': '暂无未来计划',
      'empty_filtered': '没有符合条件的项目',

      // Task meta
      'created_prefix': '创建于:',
      'updated_prefix': '更新于:',
      'reviewed_prefix': 'Reviewed:',
      'blocked_badge': '🚫 阻塞',
      'type_prefix': 'Type:',
      'priority_prefix': 'Priority:',
      'description': '描述',
      'subtasks': '子任务',
      'tags': '标签',
      'assignee_prefix': '责任人:',

      // Language switcher
      'language': '语言',
      'lang_en': 'English',
      'lang_zh': '中文',

      // Status labels
      'status_active': 'Active',
      'status_blocked': 'Blocked',
      'status_pending': 'Pending',
      'status_in_progress': '进行中',
      'status_todo': '待开始',

      // Drag and drop
      'drag_to_reorder': '拖拽排序',

      // Copy menu
      'copy_task_to': '复制任务到...',
      'copy_project_to': '复制项目到...',
      'quadrants': '四象限',
      'delegation': '委派',
      'delegation_list': '委派清单',
      'customer_projects': '客户项目',
      'cancel': '取消',

      // Move menu
      'move_task_to': '移动任务到...',
      'target_list': '目标清单',
      'maybe_list': '未来清单'
    }
  },



  /**
   * Get translation for a key
   */
  t(key) {
    const lang = this.currentLang;
    if (this.translations[lang] && this.translations[lang][key]) {
      return this.translations[lang][key];
    }
    // Fallback to English
    if (this.translations['en'][key]) {
      return this.translations['en'][key];
    }
    // Return key if not found
    return key;
  },

  /**
   * Set language and persist to localStorage
   */
  setLanguage(lang) {
    if (this.translations[lang]) {
      this.currentLang = lang;
      localStorage.setItem('dashboard-lang', lang);
      this.updatePageLanguage();
      return true;
    }
    return false;
  },

  /**
   * Get current language
   */
  getLanguage() {
    return this.currentLang;
  },

  /**
   * Update all elements with data-i18n attribute
   */
  updatePageLanguage() {
    // Update data-i18n elements
    document.querySelectorAll('[data-i18n]').forEach(el => {
      const key = el.getAttribute('data-i18n');
      const translation = this.t(key);

      // Check if element has child elements (preserve structure)
      if (el.children.length > 0 && el.textContent.trim() !== translation) {
        // Update only text nodes, preserve child elements
        const textNodes = Array.from(el.childNodes).filter(
          node => node.nodeType === Node.TEXT_NODE && node.textContent.trim()
        );
        if (textNodes.length > 0) {
          textNodes.forEach(node => {
            node.textContent = translation;
          });
        } else {
          // If no text nodes found, check for value attribute (inputs)
          if (el.hasAttribute('value')) {
            el.setAttribute('value', translation);
          } else if (el.hasAttribute('placeholder')) {
            el.setAttribute('placeholder', translation);
          }
        }
      } else {
        // Simple text content update
        el.textContent = translation;
      }
    });

    // Update data-i18n-placeholder elements
    document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
      const key = el.getAttribute('data-i18n-placeholder');
      el.setAttribute('placeholder', this.t(key));
    });

    // Update data-i18n-title elements (tooltips)
    document.querySelectorAll('[data-i18n-title]').forEach(el => {
      const key = el.getAttribute('data-i18n-title');
      el.setAttribute('title', this.t(key));
    });

    // Update HTML lang attribute
    document.documentElement.lang = this.currentLang === 'zh-CN' ? 'zh-CN' : 'en';
  },

  /**
   * Initialize language on page load
   */
  init() {
    this.updatePageLanguage();
  }
};

// Auto-initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
  i18n.init();
});

// Export for use in other scripts
if (typeof module !== 'undefined' && module.exports) {
  module.exports = i18n;
}


path: dashboard/public/index.html
content: <!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Eisenhower Task Dashboard</title>
  <link rel="icon" type="image/svg+xml" href="favicon.svg">
  <link rel="stylesheet" href="style.css?v=27">
</head>
<body>
  <div class="app">
    <header class="header">
      <h1 data-i18n="title">📊 Eisenhower Task Dashboard</h1>
      <div class="header-right">
        <div class="language-switcher">
          <button class="lang-btn" id="langBtn" data-current="en">
            <span class="lang-icon">🌐</span>
            <span class="lang-text">EN</span>
          </button>
          <div class="lang-dropdown" id="langDropdown">
            <button class="lang-option" data-lang="en" data-i18n="lang_en">English</button>
            <button class="lang-option" data-lang="zh-CN" data-i18n="lang_zh">中文</button>
          </div>
        </div>
        <div class="connection-status" id="connectionStatus">
          <span class="status-dot"></span>
          <span class="status-text" data-i18n="status_connecting">Connecting...</span>
        </div>
      </div>
    </header>

    <div class="stats-bar" id="statsBar">
      <div class="stat-card q1">
        <div class="stat-value" id="q1Count">-</div>
        <div class="stat-label" data-i18n="q1_label">🔥 Q1 Urgent & Important</div>
      </div>
      <div class="stat-card q2">
        <div class="stat-value" id="q2Count">-</div>
        <div class="stat-label" data-i18n="q2_label">💼 Q2 Important Not Urgent</div>
      </div>
      <div class="stat-card q3">
        <div class="stat-value" id="q3Count">-</div>
        <div class="stat-label" data-i18n="q3_label">⚡ Q3 Urgent Not Important</div>
      </div>
      <div class="stat-card q4">
        <div class="stat-value" id="q4Count">-</div>
        <div class="stat-label" data-i18n="q4_label">🧘 Q4 Not Urgent & Important</div>
      </div>
      <div class="stat-card customer">
        <div class="stat-value" id="customerCount">-</div>
        <div class="stat-label" data-i18n="customer_label">🏢 Customer Projects</div>
      </div>
      <div class="stat-card delegation">
        <div class="stat-value" id="delegationCount">-</div>
        <div class="stat-label" data-i18n="delegation_label">👑 Delegation Tasks</div>
      </div>
      <div class="stat-card maybe">
        <div class="stat-value" id="maybeCount">-</div>
        <div class="stat-label" data-i18n="maybe_label">🌱 Maybe List</div>
      </div>
    </div>

    <nav class="nav-tabs">
      <button class="tab-btn active" data-tab="matrix" data-i18n="tab_matrix">Eisenhower Matrix</button>
      <button class="tab-btn" data-tab="customers" data-i18n="tab_customers">Customer Projects</button>
      <button class="tab-btn" data-tab="delegation" data-i18n="tab_delegation">Delegation</button>
      <button class="tab-btn" data-tab="maybe" data-i18n="tab_maybe">Maybe List</button>
    </nav>

    <main class="main-content">
      <!-- Matrix View -->
      <div class="tab-content active" id="matrix">
        <div class="quadrant-grid">
          <div class="quadrant q1-section">
            <h2 data-i18n="q1_title">🔥 Q1: Urgent & Important</h2>
            <p class="quadrant-subtitle" data-i18n="q1_subtitle">Do First</p>
            <div class="task-list" id="q1List"></div>
          </div>
          <div class="quadrant q2-section">
            <h2 data-i18n="q2_title">💼 Q2: Important Not Urgent</h2>
            <p class="quadrant-subtitle" data-i18n="q2_subtitle">Schedule</p>
            <div class="task-list" id="q2List"></div>
          </div>
          <div class="quadrant q3-section">
            <h2 data-i18n="q3_title">⚡ Q3: Not Important Urgent</h2>
            <p class="quadrant-subtitle" data-i18n="q3_subtitle">Batch / Delegate</p>
            <div class="task-list" id="q3List"></div>
          </div>
          <div class="quadrant q4-section">
            <h2 data-i18n="q4_title">🧘 Q4: Not Urgent & Important</h2>
            <p class="quadrant-subtitle" data-i18n="q4_subtitle">Delete / Postpone</p>
            <div class="task-list" id="q4List"></div>
          </div>
        </div>
      </div>

      <!-- Customers View -->
      <div class="tab-content" id="customers">
        <div class="section-header">
          <h2 data-i18n="customer_title">🏢 Customer Project List</h2>
          <div class="filter-bar">
            <button class="filter-btn active" data-filter="all" data-i18n="filter_all">All</button>
            <button class="filter-btn" data-filter="active" data-i18n="filter_active">Active</button>
            <button class="filter-btn" data-filter="blocked" data-i18n="filter_blocked">Blocked</button>
          </div>
        </div>
        <div class="customer-list" id="customerList"></div>
      </div>

      <!-- Delegation View -->
      <div class="tab-content" id="delegation">
        <div class="section-header">
          <h2 data-i18n="delegation_title">👑 Delegation Task List</h2>
          <div class="filter-bar">
            <button class="filter-btn active" data-filter="all" data-i18n="filter_all">All</button>
            <button class="filter-btn" data-filter="inProgress" data-i18n="filter_inprogress">In Progress</button>
            <button class="filter-btn" data-filter="overdue" data-i18n="filter_overdue">Overdue</button>
          </div>
        </div>
        <div class="delegation-list" id="delegationList"></div>
      </div>

      <!-- Maybe View -->
      <div class="tab-content" id="maybe">
        <div class="section-header">
          <h2 data-i18n="maybe_title">🌱 Maybe List</h2>
          <p class="section-desc" data-i18n="maybe_desc">Ideas and inspirations for future consideration, not yet in the matrix</p>
        </div>
        <div class="maybe-list" id="maybeList"></div>
      </div>
    </main>

    <footer class="footer">
      <p><span data-i18n="last_updated">Last updated:</span> <span id="lastUpdate">-</span></p>
    </footer>
  </div>

  <script src="i18n.js?v=8"></script>
  <script src="app.js?v=26"></script>
</body>
</html>


path: dashboard/public/style.css
content: /* Eisenhower Task Dashboard Styles */

:root {
  --bg-primary: #0d1117;
  --bg-secondary: #161b22;
  --bg-tertiary: #21262d;
  --bg-hover: #30363d;
  
  --text-primary: #e6edf3;
  --text-secondary: #8b949e;
  --text-muted: #6e7681;
  
  --q1-color: #ff6b6b;
  --q2-color: #4ecdc4;
  --q3-color: #ffe66d;
  --q4-color: #95a5a6;
  --customer-color: #3498db;
  --delegation-color: #9b59b6;
  --maybe-color: #2ecc71;
  
  --border-color: #30363d;
  --border-radius: 8px;
  --shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  background: var(--bg-primary);
  color: var(--text-primary);
  line-height: 1.6;
  min-height: 100vh;
}

.app {
  max-width: 1600px;
  margin: 0 auto;
  padding: 12px 20px;
}

/* Header - Compact */
.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 0;
  border-bottom: 1px solid var(--border-color);
  margin-bottom: 12px;
}

.header h1 {
  font-size: 1.4rem;
  font-weight: 600;
}

.header-right {
  display: flex;
  align-items: center;
  gap: 12px;
}

/* Language Switcher */
.language-switcher {
  position: relative;
}

.lang-btn {
  display: flex;
  align-items: center;
  gap: 4px;
  background: var(--bg-secondary);
  border: 1px solid var(--border-color);
  color: var(--text-primary);
  padding: 6px 10px;
  font-size: 0.8rem;
  cursor: pointer;
  border-radius: var(--border-radius);
  transition: all 0.2s;
}

.lang-btn:hover {
  background: var(--bg-tertiary);
  border-color: var(--text-muted);
}

.lang-icon {
  font-size: 1rem;
}

.lang-text {
  font-weight: 500;
  min-width: 32px;
}

.lang-dropdown {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  background: var(--bg-secondary);
  border: 1px solid var(--border-color);
  border-radius: var(--border-radius);
  padding: 4px;
  min-width: 120px;
  box-shadow: var(--shadow);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px);
  transition: all 0.2s ease;
  z-index: 1000;
}

.lang-dropdown.show {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.lang-option {
  display: block;
  width: 100%;
  padding: 8px 12px;
  background: transparent;
  border: none;
  color: var(--text-secondary);
  font-size: 0.85rem;
  text-align: left;
  cursor: pointer;
  border-radius: 4px;
  transition: all 0.15s;
}

.lang-option:hover {
  background: var(--bg-tertiary);
  color: var(--text-primary);
}

.connection-status {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  background: var(--bg-secondary);
  border-radius: var(--border-radius);
}

.status-text {
  font-size: 0.75rem;
  color: var(--text-secondary);
}

.status-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--text-muted);
  transition: background 0.3s;
}

.status-dot.connected {
  background: #2ecc71;
  box-shadow: 0 0 8px #2ecc71;
}

.status-dot.disconnected {
  background: #e74c3c;
}

.status-text {
  font-size: 0.875rem;
  color: var(--text-secondary);
}

/* Stats Bar - Compact */
.stats-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 16px;
}

.stat-card {
  background: var(--bg-secondary);
  border-radius: 6px;
  padding: 8px 12px;
  text-align: center;
  border-left: 3px solid;
  transition: transform 0.2s, box-shadow 0.2s;
  flex: 1;
  min-width: 100px;
  max-width: 140px;
}

.stat-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow);
}

.stat-card.q1 { border-left-color: var(--q1-color); }
.stat-card.q2 { border-left-color: var(--q2-color); }
.stat-card.q3 { border-left-color: var(--q3-color); }
.stat-card.q4 { border-left-color: var(--q4-color); }
.stat-card.customer { border-left-color: var(--customer-color); }
.stat-card.delegation { border-left-color: var(--delegation-color); }
.stat-card.maybe { border-left-color: var(--maybe-color); }

.stat-value {
  font-size: 1.4rem;
  font-weight: 700;
  margin-bottom: 2px;
}

.stat-card.q1 .stat-value { color: var(--q1-color); }
.stat-card.q2 .stat-value { color: var(--q2-color); }
.stat-card.q3 .stat-value { color: var(--q3-color); }
.stat-card.q4 .stat-value { color: var(--q4-color); }
.stat-card.customer .stat-value { color: var(--customer-color); }
.stat-card.delegation .stat-value { color: var(--delegation-color); }
.stat-card.maybe .stat-value { color: var(--maybe-color); }

.stat-label {
  font-size: 0.7rem;
  color: var(--text-secondary);
  white-space: nowrap;
}

/* Navigation Tabs - Compact */
.nav-tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 16px;
  border-bottom: 1px solid var(--border-color);
  padding-bottom: 6px;
}

.tab-btn {
  background: transparent;
  border: none;
  color: var(--text-secondary);
  padding: 6px 14px;
  font-size: 0.85rem;
  cursor: pointer;
  border-radius: 6px;
  transition: all 0.2s;
}

.tab-btn:hover {
  background: var(--bg-secondary);
  color: var(--text-primary);
}

.tab-btn.active {
  background: var(--bg-tertiary);
  color: var(--text-primary);
  font-weight: 500;
}

/* Tab Content */
.tab-content {
  display: none;
}

.tab-content.active {
  display: block;
  animation: fadeIn 0.3s ease;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Quadrant Grid */
.quadrant-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
}

.quadrant {
  background: var(--bg-secondary);
  border-radius: var(--border-radius);
  padding: 20px;
  border-top: 4px solid;
}

.quadrant.q1-section { border-top-color: var(--q1-color); }
.quadrant.q2-section { border-top-color: var(--q2-color); }
.quadrant.q3-section { border-top-color: var(--q3-color); }
.quadrant.q4-section { border-top-color: var(--q4-color); }

.quadrant h2 {
  font-size: 1.1rem;
  margin-bottom: 4px;
}

.quadrant.q1-section h2 { color: var(--q1-color); }
.quadrant.q2-section h2 { color: var(--q2-color); }
.quadrant.q3-section h2 { color: var(--q3-color); }
.quadrant.q4-section h2 { color: var(--q4-color); }

.quadrant-subtitle {
  font-size: 0.8rem;
  color: var(--text-muted);
  margin-bottom: 16px;
}

.task-list {
  max-height: 500px; /* increased to show more tasks */
  overflow-y: auto;
  min-height: 40px;
  transition: background-color 0.2s;
}

.task-list.drag-over {
  background-color: rgba(48, 54, 61, 0.5);
  border-radius: var(--border-radius);
  box-shadow: inset 0 0 0 2px var(--border-color);
}

.task-list.drag-over.q1-list {
  box-shadow: inset 0 0 0 2px var(--q1-color);
}

.task-list.drag-over.q2-list {
  box-shadow: inset 0 0 0 2px var(--q2-color);
}

.task-list.drag-over.q3-list {
  box-shadow: inset 0 0 0 2px var(--q3-color);
}

.task-list.drag-over.q4-list {
  box-shadow: inset 0 0 0 2px var(--q4-color);
}

/* Task Cards - Compact Mode */
.task-card {
  position: relative;
  background: var(--bg-tertiary);
  border-radius: 6px;
  padding: 6px 20px 6px 10px; /* reduced padding */
  margin-bottom: 3px; /* reduced margin */
  border-left: 3px solid transparent;
  transition: all 0.2s;
}

.task-card:hover {
  background: var(--bg-hover);
  transform: translateX(4px);
}

.task-card.dragging {
  opacity: 0.5;
  transform: scale(0.95);
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
}

/* Drag handle for task cards - Compact */
.drag-handle {
  position: absolute;
  left: 4px;
  top: 50%;
  transform: translateY(-50%);
  width: 18px;
  height: 24px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  cursor: grab;
  opacity: 0.4;
  transition: all 0.2s ease;
  border-radius: 3px;
  background: var(--bg-secondary);
  border: 1px solid var(--border-color);
}

.drag-handle:hover {
  opacity: 1;
  background: var(--bg-hover);
  border-color: var(--text-muted);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}

.drag-handle:active {
  cursor: grabbing;
  background: var(--q2-color);
  border-color: var(--q2-color);
}

/* Six-dot grid pattern */
.drag-handle::before {
  content: "";
  width: 12px;
  height: 12px;
  background-image: radial-gradient(circle, var(--text-muted) 1.5px, transparent 1.5px);
  background-size: 4px 4px;
  background-position: 0 0, 0 4px, 4px 0, 4px 4px;
}

.task-card {
  position: relative;
  padding-left: 28px; /* Make room for drag handle - compact */
}

.task-card.dragging .drag-handle {
  opacity: 1;
  background: var(--q2-color);
  border-color: var(--q2-color);
}

.task-card.dragging .drag-handle::before {
  background-image: radial-gradient(circle, #fff 1.5px, transparent 1.5px);
}

.task-card.blocked {
  border-left-color: #e74c3c;
  opacity: 0.8;
}

.task-card.priority-p0 { border-left-color: #e74c3c; }
.task-card.priority-p1 { border-left-color: #f39c12; }
.task-card.priority-p2 { border-left-color: #3498db; }

/* Customer Project Drag and Drop Styles - Compact (match task-card) */
.customer-drag-handle {
  position: absolute;
  left: 4px;
  top: 50%;
  transform: translateY(-50%);
  width: 18px;
  height: 24px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  cursor: grab;
  opacity: 0.4;
  transition: all 0.2s ease;
  border-radius: 3px;
  background: var(--bg-secondary);
  border: 1px solid var(--border-color);
  user-select: none;
  -webkit-user-select: none;
  z-index: 10;
}

.customer-drag-handle:hover {
  opacity: 1;
  background: var(--bg-hover);
  border-color: var(--text-muted);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}

.customer-drag-handle:active {
  cursor: grabbing;
  background: var(--customer-color);
  border-color: var(--customer-color);
}

/* Six-dot grid pattern for customer drag handle */
.customer-drag-handle::before {
  content: "";
  width: 12px;
  height: 12px;
  background-image: radial-gradient(circle, var(--text-muted) 1.5px, transparent 1.5px);
  background-size: 4px 4px;
  background-position: 0 0, 0 4px, 4px 0, 4px 4px;
}

.project-item {
  position: relative;
  transition: all 0.2s ease;
}

.project-item.dragging {
  opacity: 0.5;
  transform: scale(0.98);
}

.project-item.dragging .customer-drag-handle {
  opacity: 1;
  background: var(--customer-color);
  border-color: var(--customer-color);
}

.project-item.dragging .customer-drag-handle::before {
  background-image: radial-gradient(circle, #fff 1.5px, transparent 1.5px);
}

.projects-list {
  min-height: 50px; /* Ensure drop zone is always visible */
}

.projects-list.drag-over {
  background: rgba(52, 152, 219, 0.15);
  border-radius: var(--border-radius);
  box-shadow: inset 0 0 0 2px var(--customer-color);
}

/* Grid drop indicator - placeholder style */
.grid-drop-placeholder {
  background: rgba(155, 89, 182, 0.2);
  border: 2px dashed var(--delegation-color);
  border-radius: 5px;
  min-height: 60px;
  animation: pulse 1s ease-in-out infinite;
}

.maybe-list .grid-drop-placeholder {
  background: rgba(46, 204, 113, 0.2);
  border-color: var(--maybe-color);
  min-height: 50px;
}

.task-title-row {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 2px;
  flex-wrap: wrap;
}

.task-id {
  font-size: 0.65rem;
  color: var(--text-muted);
  background: var(--bg-primary);
  padding: 1px 5px;
  border-radius: 8px;
  flex-shrink: 0;
}

.task-priority {
  font-size: 0.6rem;
  font-weight: 600;
  padding: 1px 5px;
  border-radius: 3px;
  text-transform: uppercase;
  flex-shrink: 0;
}

.task-priority.p0 { background: rgba(231, 76, 60, 0.2); color: #e74c3c; }
.task-priority.p1 { background: rgba(243, 156, 18, 0.2); color: #f39c12; }
.task-priority.p2 { background: rgba(52, 152, 219, 0.2); color: #3498db; }

.task-title-text {
  font-size: 0.9rem;
  font-weight: 500;
  line-height: 1.3;
  flex: 1;
  min-width: 0;
}

/* Legacy - kept for compatibility */
.task-header {
  display: none;
}

.task-title {
  display: none;
}

.task-desc {
  font-size: 0.8rem;
  color: var(--text-secondary);
  margin-bottom: 4px;
  display: -webkit-box;
  -webkit-line-clamp: 1; /* show only 1 line in compact mode */
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.task-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.task-tag {
  font-size: 0.65rem;
  padding: 1px 6px;
  background: var(--bg-hover);
  border-radius: 3px;
  color: var(--text-secondary);
}

.task-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 4px;
  font-size: 0.7rem;
  color: var(--text-muted);
}

.blocked-badge {
  background: rgba(231, 76, 60, 0.2);
  color: #e74c3c;
  padding: 1px 6px;
  border-radius: 3px;
  font-size: 0.65rem;
}

/* Task Tooltip */
.task-tooltip {
  position: fixed;
  background: var(--bg-secondary);
  border: 1px solid var(--border-color);
  border-radius: var(--border-radius);
  padding: 16px;
  min-width: 320px;
  max-width: 450px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px);
  transition: opacity 0.2s, visibility 0.2s, transform 0.2s;
  z-index: 9999;
  pointer-events: none;
}

.task-tooltip.show {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.tooltip-header {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 12px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border-color);
}

.tooltip-id {
  font-size: 0.75rem;
  color: var(--text-muted);
  background: var(--bg-tertiary);
  padding: 2px 8px;
  border-radius: 12px;
}

.tooltip-priority {
  font-size: 0.7rem;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 4px;
  text-transform: uppercase;
}

.tooltip-priority.p0 { background: rgba(231, 76, 60, 0.2); color: #e74c3c; }
.tooltip-priority.p1 { background: rgba(243, 156, 18, 0.2); color: #f39c12; }
.tooltip-priority.p2 { background: rgba(52, 152, 219, 0.2); color: #3498db; }

.tooltip-blocked {
  font-size: 0.7rem;
  color: #e74c3c;
  background: rgba(231, 76, 60, 0.15);
  padding: 2px 8px;
  border-radius: 4px;
  margin-left: auto;
}

.tooltip-title {
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: 12px;
  line-height: 1.4;
}

.tooltip-section {
  margin-bottom: 12px;
}

.tooltip-label {
  font-size: 0.75rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 6px;
}

.tooltip-desc {
  font-size: 0.9rem;
  color: var(--text-secondary);
  line-height: 1.6;
  white-space: pre-wrap;
}

.tooltip-subtasks {
  background: var(--bg-tertiary);
  border-radius: var(--border-radius);
  padding: 10px 12px;
}

.tooltip-subtask {
  font-size: 0.85rem;
  color: var(--text-secondary);
  padding: 4px 0;
  border-bottom: 1px solid var(--border-color);
}

.tooltip-subtask:last-child {
  border-bottom: none;
}

.tooltip-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.tooltip-tag {
  font-size: 0.75rem;
  padding: 4px 10px;
  background: var(--bg-tertiary);
  border-radius: 4px;
  color: var(--text-secondary);
}

.tooltip-meta {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border-color);
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 0.8rem;
  color: var(--text-muted);
}

/* Section Headers - Compact */
.section-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 14px;
  flex-wrap: wrap;
  gap: 10px;
}

.section-header h2 {
  font-size: 1.2rem;
}

.section-desc {
  color: var(--text-secondary);
  font-size: 0.85rem;
  margin-top: 2px;
}

/* Filter Bar */
.filter-bar {
  display: flex;
  gap: 8px;
}

.filter-btn {
  background: var(--bg-secondary);
  border: 1px solid var(--border-color);
  color: var(--text-secondary);
  padding: 6px 16px;
  font-size: 0.85rem;
  cursor: pointer;
  border-radius: var(--border-radius);
  transition: all 0.2s;
}

.filter-btn:hover {
  background: var(--bg-tertiary);
  color: var(--text-primary);
}

.filter-btn.active {
  background: var(--bg-tertiary);
  color: var(--text-primary);
  border-color: var(--text-muted);
}

/* Customer List - Ultra Compact (match task-card) */
.customer-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.customer-card {
  background: var(--bg-secondary);
  border-radius: 6px;
  padding: 6px 10px;
  border-left: 3px solid var(--customer-color);
}

.customer-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 4px;
}

.customer-name {
  font-size: 0.9rem;
  font-weight: 600;
}

.customer-priority {
  font-size: 0.6rem;
  padding: 1px 5px;
  border-radius: 8px;
  background: var(--bg-tertiary);
}

.projects-list {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.project-item {
  position: relative;
  background: var(--bg-tertiary);
  border-radius: var(--border-radius);
  padding: 12px 28px 12px 48px; /* top right bottom left - left for drag handle, right for delete */
}

.project-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
}

.project-name {
  font-weight: 500;
}

.project-status {
  font-size: 0.75rem;
  padding: 2px 8px;
  border-radius: 4px;
}

.project-status.active { background: rgba(46, 204, 113, 0.2); color: #2ecc71; }
.project-status.blocked { background: rgba(231, 76, 60, 0.2); color: #e74c3c; }
.project-status.pending { background: rgba(52, 152, 219, 0.2); color: #3498db; }

.project-meta {
  display: flex;
  gap: 16px;
  font-size: 0.8rem;
  color: var(--text-secondary);
  flex-wrap: wrap;
}

.project-notes {
  margin-top: 8px;
  font-size: 0.85rem;
  color: var(--text-secondary);
}

/* Delegation List - Grid Layout with clear drag indicators */
.delegation-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 8px;
  position: relative;
}

.delegation-card {
  position: relative;
  background: var(--bg-tertiary);
  border-radius: 5px;
  padding: 8px 42px 8px 28px; /* right padding for buttons, left for drag */
  border-left: 3px solid var(--delegation-color);
  transition: all 0.2s;
  font-size: 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-height: 60px;
}

.delegation-card.drag-over {
  box-shadow: 0 0 0 3px var(--delegation-color), 0 4px 12px rgba(155, 89, 182, 0.3);
  transform: scale(1.02);
}

.delegation-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow);
}

.delegation-card.overdue {
  border-left-color: #e74c3c;
}

.delegation-header {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1;
  min-width: 0;
}

.delegation-title {
  font-weight: 500;
  flex: 1;
  font-size: 0.9rem;
  line-height: 1.3;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 6px;
  overflow: hidden;
}

.delegation-title .task-id {
  flex-shrink: 0;
}

.delegation-title-text {
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  white-space: normal;
}

.delegation-status {
  font-size: 0.65rem;
  padding: 2px 6px;
  border-radius: 3px;
  text-transform: uppercase;
  flex-shrink: 0;
}

.delegation-status.todo { background: var(--bg-secondary); color: var(--text-secondary); }
.delegation-status.inprogress { background: rgba(243, 156, 18, 0.2); color: #f39c12; }

.delegation-desc {
  font-size: 0.8rem;
  color: var(--text-secondary);
  line-height: 1.3;
  flex: 1 1 100%;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  white-space: normal;
}

.delegation-meta {
  display: flex;
  align-items: center;
  gap: 16px;
  font-size: 0.75rem;
  flex-shrink: 0;
}

.delegation-assignee {
  color: var(--delegation-color);
  font-weight: 500;
  white-space: nowrap;
}

.delegation-deadline {
  color: var(--text-muted);
  white-space: nowrap;
}

.delegation-deadline.overdue {
  color: #e74c3c;
}

/* Maybe List - Grid Layout with clear drag indicators */
.maybe-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 8px;
  position: relative;
}

.maybe-card {
  position: relative;
  background: var(--bg-tertiary);
  border-radius: 5px;
  padding: 8px 58px 8px 28px; /* right padding for buttons, left for drag */
  border-left: 3px solid var(--maybe-color);
  transition: all 0.2s;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-height: 50px;
}

.maybe-card.drag-over {
  box-shadow: 0 0 0 3px var(--maybe-color), 0 4px 12px rgba(46, 204, 113, 0.3);
  transform: scale(1.02);
}

.maybe-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow);
}

.maybe-header {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1;
  min-width: 0;
}

.maybe-category {
  font-size: 0.65rem;
  color: var(--maybe-color);
  text-transform: uppercase;
  flex-shrink: 0;
}

.maybe-title {
  font-weight: 500;
  font-size: 0.9rem;
  line-height: 1.3;
  flex: 1;
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  overflow: hidden;
}

.maybe-title .task-id {
  flex-shrink: 0;
}

.maybe-title-text {
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  white-space: normal;
}

.maybe-desc {
  font-size: 0.8rem;
  color: var(--text-secondary);
  line-height: 1.3;
  flex: 1 1 100%;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  white-space: normal;
}

/* Footer */
.footer {
  margin-top: 40px;
  padding: 20px 0;
  text-align: center;
  border-top: 1px solid var(--border-color);
  color: var(--text-muted);
  font-size: 0.85rem;
}

/* Scrollbar */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

::-webkit-scrollbar-track {
  background: var(--bg-primary);
}

::-webkit-scrollbar-thumb {
  background: var(--bg-tertiary);
  border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--bg-hover);
}

/* Responsive */
@media (max-width: 1200px) {
  .quadrant-grid {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 768px) {
  .app {
    padding: 12px;
  }
  
  .header {
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
  }
  
  .stats-bar {
    grid-template-columns: repeat(2, 1fr);
  }
  
  .nav-tabs {
    flex-wrap: wrap;
  }
  
  .tab-btn {
    padding: 8px 12px;
    font-size: 0.85rem;
  }
  
  .section-header {
    flex-direction: column;
    align-items: flex-start;
  }
  
  .delegation-list,
  .maybe-list {
    grid-template-columns: 1fr;
  }
}


}

/* Empty State */
.empty-state {
  text-align: center;
  padding: 40px;
  color: var(--text-muted);
}

/* Drag Preview */
.drag-preview {
  position: fixed;
  pointer-events: none;
  z-index: 10000;
  opacity: 0.9;
  transform: rotate(3deg);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}

/* Drop Indicator - for list layouts */
.drop-indicator {
  height: 4px;
  background: linear-gradient(90deg, var(--q2-color), var(--q1-color));
  border-radius: 2px;
  margin: 2px 0;
  animation: pulse 0.8s ease-in-out infinite;
}

/* Customer Project Drop Indicator */
.customer-drop-indicator {
  background: linear-gradient(90deg, var(--customer-color), #5dade2);
  height: 4px;
  box-shadow: 0 0 12px var(--customer-color), 0 0 4px var(--customer-color);
}

/* Grid layout uses card highlight instead of line indicator */
.delegation-drop-indicator,
.maybe-drop-indicator {
  display: none;
}

@keyframes pulse {
  0%, 100% { opacity: 0.6; }
  50% { opacity: 1; }
}

.empty-state-icon {
  font-size: 3rem;
  margin-bottom: 12px;
}

/* Action Buttons (Delete, Copy, Move, Complete) */
.delete-btn, .copy-btn, .move-btn, .complete-btn {
  position: absolute;
  top: 4px;
  width: 18px;
  height: 18px;
  border: none;
  background: transparent;
  color: var(--text-muted);
  font-size: 14px;
  line-height: 18px;
  text-align: center;
  cursor: pointer;
  border-radius: 3px;
  opacity: 0;
  transition: all 0.2s ease;
  z-index: 10;
}

.delete-btn {
  right: 4px;
}

.complete-btn {
  right: 58px;
  color: var(--maybe-color);
  font-weight: bold;
}

.copy-btn {
  right: 40px;
}

.move-btn {
  right: 22px;
  font-size: 12px;
}

.complete-btn:hover {
  background: var(--maybe-color);
  color: white;
  opacity: 1;
}

.copy-btn:hover {
  background: var(--q2-color);
  color: white;
  opacity: 1;
}

.move-btn:hover {
  background: var(--q2-color);
  color: white;
  opacity: 1;
}

.delete-btn:hover {
  background: #e74c3c;
  color: white;
  opacity: 1;
}

/* Show action buttons on card hover */
.task-card:hover .delete-btn,
.task-card:hover .copy-btn,
.task-card:hover .move-btn,
.task-card:hover .complete-btn,
.project-item:hover .delete-btn,
.project-item:hover .copy-btn,
.project-item:hover .complete-btn,
.delegation-card:hover .delete-btn,
.delegation-card:hover .complete-btn,
.maybe-card:hover .delete-btn,
.maybe-card:hover .move-btn,
.maybe-card:hover .complete-btn {
  opacity: 0.6;
}

.task-card:hover .delete-btn:hover,
.task-card:hover .copy-btn:hover,
.task-card:hover .move-btn:hover,
.task-card:hover .complete-btn:hover,
.project-item:hover .delete-btn:hover,
.project-item:hover .copy-btn:hover,
.project-item:hover .complete-btn:hover,
.delegation-card:hover .delete-btn:hover,
.delegation-card:hover .complete-btn:hover,
.maybe-card:hover .delete-btn:hover,
.maybe-card:hover .move-btn:hover,
.maybe-card:hover .complete-btn:hover {
  opacity: 1;
}

/* Position for customer project buttons - match task-card */
.customer-delete-btn {
  top: 4px;
  right: 4px;
  width: 14px;
  height: 14px;
  font-size: 10px;
  line-height: 14px;
}

.customer-copy-btn {
  top: 4px;
  right: 18px;
  width: 14px;
  height: 14px;
  font-size: 10px;
  line-height: 14px;
}

.customer-complete-btn {
  top: 4px;
  right: 32px;
  width: 14px;
  height: 14px;
  font-size: 10px;
  line-height: 14px;
}

/* Position for maybe card buttons */
.maybe-delete-btn {
  top: 6px;
  right: 6px;
}

.maybe-move-btn {
  top: 6px;
  right: 24px;
  font-size: 12px;
}

/* Delegation card buttons - match task-card */
.delegation-complete-btn {
  top: 4px;
  right: 18px;
  width: 14px;
  height: 14px;
  font-size: 10px;
  line-height: 14px;
  color: var(--maybe-color);
  font-weight: bold;
}

.delegation-complete-btn:hover {
  background: var(--maybe-color);
  color: white;
}

.delegation-delete-btn {
  top: 4px;
  right: 4px;
  width: 14px;
  height: 14px;
  font-size: 10px;
  line-height: 14px;
}

/* Delegation card drag handle */
.delegation-drag-handle {
  position: absolute;
  left: 4px;
  top: 50%;
  transform: translateY(-50%);
  width: 18px;
  height: 24px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  cursor: grab;
  opacity: 0.4;
  transition: all 0.2s ease;
  border-radius: 3px;
  background: var(--bg-secondary);
  border: 1px solid var(--border-color);
}

.delegation-drag-handle:hover {
  opacity: 0.8;
  background: var(--bg-hover);
}

.delegation-drag-handle::before {
  content: "";
  width: 12px;
  height: 12px;
  background-image: radial-gradient(circle, var(--text-muted) 1.5px, transparent 1.5px);
  background-size: 4px 4px;
  background-position: 0 0, 0 4px, 4px 0, 4px 4px;
}

.delegation-card {
  position: relative;
  padding-left: 28px;
  padding-right: 42px;
}

.delegation-card.dragging {
  opacity: 0.5;
}

/* Maybe card buttons - match task-card */
.maybe-complete-btn {
  top: 4px;
  right: 32px;
  width: 14px;
  height: 14px;
  font-size: 10px;
  line-height: 14px;
  color: var(--maybe-color);
  font-weight: bold;
}

.maybe-complete-btn:hover {
  background: var(--maybe-color);
  color: white;
}

.maybe-move-btn {
  top: 4px;
  right: 18px;
  width: 14px;
  height: 14px;
  font-size: 9px;
  line-height: 14px;
}

.maybe-delete-btn {
  top: 4px;
  right: 4px;
  width: 14px;
  height: 14px;
  font-size: 10px;
  line-height: 14px;
}

/* Maybe card drag handle */
.maybe-drag-handle {
  position: absolute;
  left: 4px;
  top: 50%;
  transform: translateY(-50%);
  width: 18px;
  height: 24px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  cursor: grab;
  opacity: 0.4;
  transition: all 0.2s ease;
  border-radius: 3px;
  background: var(--bg-secondary);
  border: 1px solid var(--border-color);
}

.maybe-drag-handle:hover {
  opacity: 0.8;
  background: var(--bg-hover);
}

.maybe-drag-handle::before {
  content: "";
  width: 12px;
  height: 12px;
  background-image: radial-gradient(circle, var(--text-muted) 1.5px, transparent 1.5px);
  background-size: 4px 4px;
  background-position: 0 0, 0 4px, 4px 0, 4px 4px;
}

.maybe-card {
  position: relative;
  padding-left: 28px;
  padding-right: 58px;
}

.maybe-card.dragging {
  opacity: 0.5;
}

/* Copy Menu Modal */
.copy-menu-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10000;
  opacity: 0;
  visibility: hidden;
  transition: all 0.2s ease;
}

.copy-menu-overlay.show {
  opacity: 1;
  visibility: visible;
}

.copy-menu {
  background: var(--bg-secondary);
  border-radius: var(--border-radius);
  padding: 20px;
  min-width: 320px;
  max-width: 400px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
  border: 1px solid var(--border-color);
}

.copy-menu-header {
  font-size: 1.1rem;
  font-weight: 600;
  margin-bottom: 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border-color);
}

.copy-menu-section {
  margin-bottom: 16px;
}

.copy-menu-label {
  font-size: 0.8rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 8px;
}

.copy-menu-options {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.copy-menu-btn {
  background: var(--bg-tertiary);
  border: 1px solid var(--border-color);
  color: var(--text-primary);
  padding: 10px 12px;
  border-radius: var(--border-radius);
  cursor: pointer;
  font-size: 0.9rem;
  transition: all 0.2s;
  text-align: left;
}

.copy-menu-btn:hover {
  background: var(--bg-hover);
  border-color: var(--text-muted);
}

.copy-menu-btn.q1 { border-left: 3px solid var(--q1-color); }
.copy-menu-btn.q2 { border-left: 3px solid var(--q2-color); }
.copy-menu-btn.q3 { border-left: 3px solid var(--q3-color); }
.copy-menu-btn.q4 { border-left: 3px solid var(--q4-color); }
.copy-menu-btn.delegation { border-left: 3px solid var(--delegation-color); }

.copy-menu-cancel {
  width: 100%;
  background: transparent;
  border: 1px solid var(--border-color);
  color: var(--text-secondary);
  padding: 10px;
  border-radius: var(--border-radius);
  cursor: pointer;
  font-size: 0.9rem;
  transition: all 0.2s;
  margin-top: 8px;
}

.copy-menu-cancel:hover {
  background: var(--bg-tertiary);
  color: var(--text-primary);
}

/* Customer Project Button Positions */
.customer-copy-btn {
  top: 4px;
  right: 44px;
}

.customer-complete-btn {
  top: 4px;
  right: 24px;
  color: var(--maybe-color);
  font-weight: bold;
}

.customer-complete-btn:hover {
  background: var(--maybe-color);
  color: white;
}

/* Compact project item - match task-card */
.project-item {
  position: relative;
  background: var(--bg-tertiary);
  border-radius: 5px;
  padding: 4px 42px 4px 28px; /* left for drag handle, right for buttons */
  margin-bottom: 3px;
  font-size: 0.85rem;
  border-left: 3px solid transparent;
}

.project-item.active { border-left-color: var(--customer-color); }
.project-item.blocked { border-left-color: #e74c3c; opacity: 0.8; }

.project-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 2px;
}

.project-name {
  font-weight: 500;
  font-size: 0.85rem;
  line-height: 1.3;
}

.project-status {
  font-size: 0.6rem;
  padding: 1px 5px;
  border-radius: 3px;
  flex-shrink: 0;
}

.project-meta {
  display: flex;
  gap: 8px;
  font-size: 0.7rem;
  color: var(--text-secondary);
  flex-wrap: wrap;
}

.project-notes {
  margin-top: 2px;
  font-size: 0.75rem;
  color: var(--text-secondary);
  line-height: 1.3;
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Custom Alert Modal */
.custom-alert-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 9999;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.2s ease, visibility 0.2s ease;
}

.custom-alert-overlay.show {
  opacity: 1;
  visibility: visible;
}

.custom-alert-modal {
  background: white;
  padding: 24px;
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
  max-width: 400px;
  width: 90%;
  text-align: center;
  transform: translateY(-20px);
  transition: transform 0.2s ease;
}

.custom-alert-overlay.show .custom-alert-modal {
  transform: translateY(0);
}

.custom-alert-message {
  font-size: 16px;
  color: #333;
  margin-bottom: 20px;
  line-height: 1.5;
}

.custom-alert-btn {
  background: var(--primary-color, #4a90e2);
  color: white;
  border: none;
  padding: 10px 24px;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.2s;
}

.custom-alert-btn:hover {
  background: var(--primary-dark, #357abd);
}

/* Custom Confirm Actions */
.custom-confirm-actions {
  display: flex;
  justify-content: center;
  gap: 12px;
}

.custom-confirm-cancel {
  background: #f1f3f5;
  color: #333;
}

.custom-confirm-cancel:hover {
  background: #e2e6ea;
}

.custom-confirm-ok {
  background: #e03131;
}

.custom-confirm-ok:hover {
  background: #c92a2a;
}


path: dashboard/server.js
content: /**
 * Eisenhower Task Dashboard Server
 * Express + WebSocket for real-time updates
 */

const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const chokidar = require('chokidar');
const path = require('path');
const fs = require('fs');
const { 
  loadAllTasks, 
  moveTask, 
  moveCustomerProject,
  deleteTask,
  deleteCustomerProject,
  deleteDelegationTask,
  deleteMaybeTask,
  copyTask,
  copyCustomerProject,
  moveMaybeTaskToQuadrant,
  moveQuadrantTaskToMaybe,
  copyTaskToCustomer,
  copyTaskToDelegation,
  moveTaskToDelegation,
  reorderDelegationTask,
  reorderMaybeTask,
  completeTask,
  completeCustomerProject,
  completeDelegationTask,
  completeMaybeTask
} = require('./parser');

// Parse command line arguments for port
function parseArgs() {
  const args = process.argv.slice(2);
  let port = 8080;

  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--port' && args[i + 1]) {
      port = parseInt(args[i + 1], 10);
      i++;
    }
  }

  return { port };
}

const { port } = parseArgs();
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

// Middleware
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));

// API endpoint to get all tasks
app.get('/api/tasks', (req, res) => {
  const data = loadAllTasks();
  res.json(data);
});

// API endpoint for health check
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// API endpoint to move/reorder a customer project
app.post('/api/customer-projects/move', async (req, res) => {
  try {
    const { projectId, sourceCustomer, targetCustomer, insertIndex } = req.body;

    console.log('[API] Move customer project request:', { projectId, sourceCustomer, targetCustomer, insertIndex });

    if (!projectId || !sourceCustomer || !targetCustomer) {
      return res.status(400).json({
        error: 'Missing required fields: projectId, sourceCustomer, targetCustomer'
      });
    }

    const result = await moveCustomerProject(projectId, sourceCustomer, targetCustomer, insertIndex);

    if (result.success) {
      res.json({ success: true, message: result.message });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Move customer project error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to move a task between quadrants or reorder within quadrant
app.post('/api/tasks/move', async (req, res) => {
  try {
    const { taskId, sourceQuadrant, targetQuadrant, insertIndex } = req.body;
    
    console.log('[API] Move task request:', { taskId, sourceQuadrant, targetQuadrant, insertIndex });
    
    if (!taskId || !sourceQuadrant || !targetQuadrant) {
      return res.status(400).json({ 
        error: 'Missing required fields: taskId, sourceQuadrant, targetQuadrant' 
      });
    }
    
    const result = await moveTask(taskId, sourceQuadrant, targetQuadrant, insertIndex);
    
    if (result.success) {
      res.json({ success: true, message: result.message });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Move task error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to delete a task from tasks.md
app.post('/api/tasks/delete', async (req, res) => {
  try {
    const { taskId, quadrant } = req.body;
    
    console.log('[API] Delete task request:', { taskId, quadrant });
    
    if (!taskId || !quadrant) {
      return res.status(400).json({ 
        error: 'Missing required fields: taskId, quadrant' 
      });
    }
    
    const result = await deleteTask(taskId, quadrant);
    
    if (result.success) {
      res.json({ success: true, message: result.message });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Delete task error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to delete a customer project
app.post('/api/customer-projects/delete', async (req, res) => {
  try {
    const { projectId, customerName } = req.body;
    
    console.log('[API] Delete customer project request:', { projectId, customerName });
    
    if (!projectId || !customerName) {
      return res.status(400).json({ 
        error: 'Missing required fields: projectId, customerName' 
      });
    }
    
    const result = await deleteCustomerProject(projectId, customerName);
    
    if (result.success) {
      res.json({ success: true, message: result.message });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Delete customer project error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to delete a delegation task
app.post('/api/delegation/delete', async (req, res) => {
  try {
    const { taskId } = req.body;
    
    console.log('[API] Delete delegation task request:', { taskId });
    
    if (!taskId) {
      return res.status(400).json({ 
        error: 'Missing required field: taskId' 
      });
    }
    
    const result = await deleteDelegationTask(taskId);
    
    if (result.success) {
      res.json({ success: true, message: result.message });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Delete delegation task error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to delete a maybe list task
app.post('/api/maybe/delete', async (req, res) => {
  try {
    const { taskId } = req.body;
    
    console.log('[API] Delete maybe task request:', { taskId });
    
    if (!taskId) {
      return res.status(400).json({ 
        error: 'Missing required field: taskId' 
      });
    }
    
    const result = await deleteMaybeTask(taskId);
    
    if (result.success) {
      res.json({ success: true, message: result.message });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Delete maybe task error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to copy a task to another quadrant
app.post('/api/tasks/copy', async (req, res) => {
  try {
    const { taskId, sourceQuadrant, target } = req.body;
    
    console.log('[API] Copy task request:', { taskId, sourceQuadrant, target });
    
    if (!taskId || !sourceQuadrant || !target) {
      return res.status(400).json({ 
        error: 'Missing required fields: taskId, sourceQuadrant, target' 
      });
    }
    
    const result = await copyTask(taskId, sourceQuadrant, target);
    
    if (result.success) {
      res.json({ success: true, message: result.message, newTaskId: result.newTaskId });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Copy task error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to copy a customer project to quadrants or delegation
app.post('/api/customer-projects/copy', async (req, res) => {
  try {
    const { projectId, sourceCustomer, target } = req.body;
    
    console.log('[API] Copy customer project request:', { projectId, sourceCustomer, target });
    
    if (!projectId || !sourceCustomer || !target) {
      return res.status(400).json({ 
        error: 'Missing required fields: projectId, sourceCustomer, target' 
      });
    }
    
    const result = await copyCustomerProject(projectId, sourceCustomer, target);
    
    if (result.success) {
      res.json({ success: true, message: result.message, newTaskId: result.newTaskId });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Copy customer project error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to move a maybe task to a quadrant
app.post('/api/maybe/move', async (req, res) => {
  try {
    const { taskId, targetQuadrant } = req.body;
    
    console.log('[API] Move maybe task request:', { taskId, targetQuadrant });
    
    if (!taskId || !targetQuadrant) {
      return res.status(400).json({ 
        error: 'Missing required fields: taskId, targetQuadrant' 
      });
    }
    
    const result = await moveMaybeTaskToQuadrant(taskId, targetQuadrant);
    
    if (result.success) {
      res.json({ success: true, message: result.message, newTaskId: result.newTaskId });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Move maybe task error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to move a quadrant task to maybe list
app.post('/api/tasks/move-to-maybe', async (req, res) => {
  try {
    const { taskId, sourceQuadrant } = req.body;
    
    console.log('[API] Move task to maybe request:', { taskId, sourceQuadrant });
    
    if (!taskId || !sourceQuadrant) {
      return res.status(400).json({ 
        error: 'Missing required fields: taskId, sourceQuadrant' 
      });
    }
    
    const result = await moveQuadrantTaskToMaybe(taskId, sourceQuadrant);
    
    if (result.success) {
      res.json({ success: true, message: result.message, newTaskId: result.newTaskId });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Move task to maybe error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to copy a quadrant task to customer project
app.post('/api/tasks/copy-to-customer', async (req, res) => {
  try {
    const { taskId, sourceQuadrant, targetCustomer } = req.body;
    
    console.log('[API] Copy task to customer request:', { taskId, sourceQuadrant, targetCustomer });
    
    if (!taskId || !sourceQuadrant || !targetCustomer) {
      return res.status(400).json({ 
        error: 'Missing required fields: taskId, sourceQuadrant, targetCustomer' 
      });
    }
    
    const result = await copyTaskToCustomer(taskId, sourceQuadrant, targetCustomer);
    
    if (result.success) {
      res.json({ success: true, message: result.message, newProjectId: result.newProjectId });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Copy task to customer error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to move a quadrant task to delegation list
app.post('/api/tasks/move-to-delegation', async (req, res) => {
  try {
    const { taskId, sourceQuadrant } = req.body;
    
    console.log('[API] Move task to delegation request:', { taskId, sourceQuadrant });
    
    if (!taskId || !sourceQuadrant) {
      return res.status(400).json({ 
        error: 'Missing required fields: taskId, sourceQuadrant' 
      });
    }
    
    const result = await moveTaskToDelegation(taskId, sourceQuadrant);
    
    if (result.success) {
      res.json({ success: true, message: result.message, newTaskId: result.newTaskId });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Move task to delegation error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to copy a quadrant task to delegation list
app.post('/api/tasks/copy-to-delegation', async (req, res) => {
  try {
    const { taskId, sourceQuadrant } = req.body;
    
    console.log('[API] Copy task to delegation request:', { taskId, sourceQuadrant });
    
    if (!taskId || !sourceQuadrant) {
      return res.status(400).json({ 
        error: 'Missing required fields: taskId, sourceQuadrant' 
      });
    }
    
    const result = await copyTaskToDelegation(taskId, sourceQuadrant);
    
    if (result.success) {
      res.json({ success: true, message: result.message, newTaskId: result.newTaskId });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Copy task to delegation error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to reorder delegation tasks
app.post('/api/delegation/reorder', async (req, res) => {
  try {
    const { taskId, insertIndex } = req.body;
    
    console.log('[API] Reorder delegation task:', { taskId, insertIndex });
    
    if (!taskId) {
      return res.status(400).json({ error: 'Missing required field: taskId' });
    }
    
    const result = await reorderDelegationTask(taskId, insertIndex);
    
    if (result.success) {
      res.json({ success: true, message: result.message });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Reorder delegation error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to reorder maybe tasks
app.post('/api/maybe/reorder', async (req, res) => {
  try {
    const { taskId, insertIndex } = req.body;
    
    console.log('[API] Reorder maybe task:', { taskId, insertIndex });
    
    if (!taskId) {
      return res.status(400).json({ error: 'Missing required field: taskId' });
    }
    
    const result = await reorderMaybeTask(taskId, insertIndex);
    
    if (result.success) {
      res.json({ success: true, message: result.message });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Reorder maybe error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to complete a task
app.post('/api/tasks/complete', async (req, res) => {
  try {
    const { taskId, quadrant } = req.body;
    
    console.log('[API] Complete task:', { taskId, quadrant });
    
    if (!taskId || !quadrant) {
      return res.status(400).json({ error: 'Missing required fields: taskId, quadrant' });
    }
    
    const result = await completeTask(taskId, quadrant);
    
    if (result.success) {
      res.json({ success: true, message: result.message });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Complete task error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to complete a customer project
app.post('/api/customer-projects/complete', async (req, res) => {
  try {
    const { projectId, customerName } = req.body;
    
    console.log('[API] Complete customer project:', { projectId, customerName });
    
    if (!projectId || !customerName) {
      return res.status(400).json({ error: 'Missing required fields: projectId, customerName' });
    }
    
    const result = await completeCustomerProject(projectId, customerName);
    
    if (result.success) {
      res.json({ success: true, message: result.message });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Complete customer project error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to complete a delegation task
app.post('/api/delegation/complete', async (req, res) => {
  try {
    const { taskId } = req.body;
    
    console.log('[API] Complete delegation task:', { taskId });
    
    if (!taskId) {
      return res.status(400).json({ error: 'Missing required field: taskId' });
    }
    
    const result = await completeDelegationTask(taskId);
    
    if (result.success) {
      res.json({ success: true, message: result.message });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Complete delegation task error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// API endpoint to complete a maybe task
app.post('/api/maybe/complete', async (req, res) => {
  try {
    const { taskId } = req.body;
    
    console.log('[API] Complete maybe task:', { taskId });
    
    if (!taskId) {
      return res.status(400).json({ error: 'Missing required field: taskId' });
    }
    
    const result = await completeMaybeTask(taskId);
    
    if (result.success) {
      res.json({ success: true, message: result.message });
    } else {
      res.status(400).json({ success: false, error: result.error });
    }
  } catch (error) {
    console.error('[API] Complete maybe task error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// WebSocket connection handling
const clients = new Set();

wss.on('connection', (ws) => {
  console.log('[WebSocket] Client connected');
  clients.add(ws);

  // Send initial data
  const initialData = loadAllTasks();
  ws.send(JSON.stringify({ type: 'init', data: initialData }));

  ws.on('close', () => {
    console.log('[WebSocket] Client disconnected');
    clients.delete(ws);
  });

  ws.on('error', (err) => {
    console.error('[WebSocket] Error:', err);
    clients.delete(ws);
  });
});

// Broadcast data to all connected clients
function broadcast(data) {
  const message = JSON.stringify({ type: 'update', data });
  clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

// Watch for file changes
// Tasks are located in workspace/tasks/ directory (4 levels up from dashboard)
const tasksDir = process.env.EISENHOWER_TASKS_DIR || path.join(__dirname, '../tasks');
const watchFiles = [
  path.join(tasksDir, 'tasks.md'),
  path.join(tasksDir, 'customer-projects.md'),
  path.join(tasksDir, 'delegation.md'),
  path.join(tasksDir, 'maybe.md')
];

console.log('[Watcher] Watching files:');
watchFiles.forEach(f => console.log(`  - ${f}`));

const watcher = chokidar.watch(watchFiles, {
  persistent: true,
  ignoreInitial: true,
  usePolling: false,
  interval: 1000
});

let debounceTimer = null;

watcher.on('change', (filePath) => {
  console.log(`[Watcher] File changed: ${path.basename(filePath)}`);

  // Debounce to avoid multiple rapid updates
  if (debounceTimer) {
    clearTimeout(debounceTimer);
  }

  debounceTimer = setTimeout(() => {
    console.log('[Watcher] Broadcasting update to clients');
    const data = loadAllTasks();
    broadcast(data);
  }, 500);
});

watcher.on('error', (error) => {
  console.error('[Watcher] Error:', error);
});

// Save port to config file for next time
const PORT_FILE = path.join(__dirname, 'port.conf');
fs.writeFileSync(PORT_FILE, port.toString());

// Start server
server.listen(port, () => {
  console.log(`
╔════════════════════════════════════════════════════════════╗
║  Eisenhower Task Dashboard                                 ║
╠════════════════════════════════════════════════════════════╣
║  Server running at: http://localhost:${port}                   ║
║  API endpoint:      http://localhost:${port}/api/tasks         ║
║  WebSocket:         ws://localhost:${port}                     ║
╚════════════════════════════════════════════════════════════╝
  `);
});

// Handle server startup errors (e.g., port already in use)
server.on('error', (err) => {
  if (err.code === 'EADDRINUSE') {
    console.error(`[Error] Port ${port} is already in use. Please use a different port with: ./start.sh --port <port>`);
  } else {
    console.error('[Error] Server error:', err);
  }
  process.exit(1);
});

// Global error handlers to prevent crashes
process.on('uncaughtException', (err) => {
  console.error('[Fatal] Uncaught Exception:', err);
  // Don't exit immediately, try to keep running
});

process.on('unhandledRejection', (reason, promise) => {
  console.error('[Fatal] Unhandled Rejection at:', promise, 'reason:', reason);
  // Don't exit immediately, try to keep running
});

// Graceful shutdown
process.on('SIGINT', () => {
  console.log('\n[Server] SIGINT received, shutting down gracefully...');
  watcher.close();
  wss.close();
  server.close(() => {
    console.log('[Server] Closed');
    process.exit(0);
  });
});

process.on('SIGTERM', () => {
  console.log('\n[Server] SIGTERM received, shutting down gracefully...');
  watcher.close();
  wss.close();
  server.close(() => {
    console.log('[Server] Closed');
    process.exit(0);
  });
});


path: dashboard/start.sh
content: #!/bin/bash
# Eisenhower Task Dashboard Startup Script
# Automatically checks/installs dependencies and remembers the last used port

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PORT_FILE="$SCRIPT_DIR/port.conf"

# Check and install dependencies if needed
if [ ! -d "$SCRIPT_DIR/node_modules" ]; then
  echo "[Dashboard] First time setup - installing dependencies..."
  bash "$SCRIPT_DIR/check-and-install.sh"
  if [ $? -ne 0 ]; then
    exit 1
  fi
fi

# Read saved port
if [ -f "$PORT_FILE" ]; then
    SAVED_PORT=$(cat "$PORT_FILE" | tr -d '[:space:]')
fi

# If we are in the development repository (detected by being inside yayayahei-skills)
# use 8081 as default to avoid clashing with the global installation
if [[ "$SCRIPT_DIR" == *"yayayahei-skills"* ]]; then
    DEFAULT_PORT=8081
else
    DEFAULT_PORT=8080
fi

SAVED_PORT=${SAVED_PORT:-$DEFAULT_PORT}

# Parse arguments
USER_PORT=""
# Preserve existing EISENHOWER_TASKS_DIR from environment
DAEMON_MODE=false
ARGS=()

while [[ "$#" -gt 0 ]]; do
    case $1 in
        --port)
            USER_PORT="$2"
            shift 2
            ;;
        --tasks-dir)
            if [ -n "$EISENHOWER_TASKS_DIR" ] && [ "$EISENHOWER_TASKS_DIR" != "$2" ]; then
                echo "[Dashboard] Note: System environment variable EISENHOWER_TASKS_DIR is set. Ignoring --tasks-dir argument."
            else
                EISENHOWER_TASKS_DIR="$2"
            fi
            shift 2
            ;;
        --daemon)
            DAEMON_MODE=true
            shift
            ;;
        *)
            ARGS+=("$1")
            shift
            ;;
    esac
done

# Determine which port to use
if [ -n "$USER_PORT" ]; then
    # User specified a port, save it for next time
    echo "$USER_PORT" > "$PORT_FILE"
    echo "[Dashboard] Port saved: $USER_PORT"
    ARGS+=("--port" "$USER_PORT")
else
    # No port specified, use saved port
    ARGS+=("--port" "$SAVED_PORT")
    echo "[Dashboard] Using saved port: $SAVED_PORT"
fi

# Export EISENHOWER_TASKS_DIR if specified
if [ -n "$EISENHOWER_TASKS_DIR" ]; then
    export EISENHOWER_TASKS_DIR="$EISENHOWER_TASKS_DIR"
    echo "[Dashboard] Using tasks directory: $EISENHOWER_TASKS_DIR"
    # Ensure it's passed to the background process in daemon mode
    ARGS+=("--tasks-dir" "$EISENHOWER_TASKS_DIR")
fi

cd "$SCRIPT_DIR"

if [ "$DAEMON_MODE" = true ]; then
    # Daemon mode: run in background with nohup
    LOG_FILE="$SCRIPT_DIR/dashboard.log"
    PID_FILE="$SCRIPT_DIR/dashboard.pid"
    
    # Check if already running
    if [ -f "$PID_FILE" ]; then
        OLD_PID=$(cat "$PID_FILE")
        if kill -0 "$OLD_PID" 2>/dev/null; then
            echo "[Dashboard] Already running (PID: $OLD_PID)"
            echo "[Dashboard] Visit: http://localhost:${USER_PORT:-$SAVED_PORT}"
            exit 0
        fi
    fi
    
    echo "[Dashboard] Starting in daemon mode..."
    echo "[Dashboard] Log file: $LOG_FILE"
    if [ -n "$EISENHOWER_TASKS_DIR" ]; then
        nohup env EISENHOWER_TASKS_DIR="$EISENHOWER_TASKS_DIR" node server.js "${ARGS[@]}" > "$LOG_FILE" 2>&1 &
    else
        nohup node server.js "${ARGS[@]}" > "$LOG_FILE" 2>&1 &
    fi
    PID=$!
    echo $PID > "$PID_FILE"
    
    # Wait a moment to check if it started successfully
    sleep 2
    if kill -0 $PID 2>/dev/null; then
        echo "[Dashboard] Started successfully (PID: $PID)"
        echo "[Dashboard] Visit: http://localhost:${USER_PORT:-$SAVED_PORT}"
        echo "[Dashboard] Stop with: kill $PID"
    else
        echo "[Dashboard] Failed to start. Check log: $LOG_FILE"
        rm -f "$PID_FILE"
        exit 1
    fi
else
    # Interactive mode: run in foreground
    node server.js "${ARGS[@]}"
fi


path: dashboard/stop.sh
content: #!/bin/bash
# Eisenhower Task Dashboard Stop Script

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PID_FILE="$SCRIPT_DIR/dashboard.pid"

if [ -f "$PID_FILE" ]; then
    PID=$(cat "$PID_FILE")
    if kill -0 "$PID" 2>/dev/null; then
        echo "[Dashboard] Stopping server (PID: $PID)..."
        kill "$PID"
        sleep 2
        if kill -0 "$PID" 2>/dev/null; then
            echo "[Dashboard] Force killing..."
            kill -9 "$PID"
        fi
        rm -f "$PID_FILE"
        echo "[Dashboard] Stopped"
    else
        echo "[Dashboard] Not running (stale PID file removed)"
        rm -f "$PID_FILE"
    fi
else
    echo "[Dashboard] Not running (no PID file found)"
fi


path: references/dashboard-offer.md
content: # Dashboard Offer Workflow

Post-operation step: Offer to launch the visual task dashboard after completing task operations.

## When to Offer

Offer dashboard after these operations:
- ✅ Add task (any level)
- ✅ Complete task
- ✅ Move task between quadrants
- ✅ Bulk operations (multiple tasks)

Skip for:
- ❌ Simple queries (view tasks, check numbering)
- ❌ Silent operations (heartbeats, auto-fixes)

## Offer Format

```markdown
✅ [Operation] Completed

**Summary:**
- Operation: Add Task
- Task: [Task Name]
- Location: Q2 #15

📊 View task dashboard?
Type 'yes' to open, or press Enter to skip.
```

## User Response Handling

### User Approves ("yes", "y", "open", "dashboard")

```bash
# 1. Navigate to dashboard directory
cd ~/.openclaw/workspace/skills/eisenhower-task-manager/dashboard

# 2. Check if already running
if pgrep -f "node server.js" > /dev/null; then
    echo "[Dashboard] Already running"
    # Just open browser
    open http://localhost:$(cat port.conf 2>/dev/null || echo 8080)
else
    # 3. Start dashboard
    ./start.sh &
    
    # 4. Wait for server to start
    sleep 3
    
    # 5. Open browser
    PORT=$(cat port.conf 2>/dev/null || echo 8080)
    open http://localhost:$PORT
fi
```

### User Declines ("no", "n", empty, any other input)

```markdown
Skipped. You can open the dashboard anytime with:
cd dashboard && ./start.sh
```

## Implementation Example

```javascript
// Pseudo-code for skill implementation
async function offerDashboard(operation, taskInfo) {
  console.log(`✅ ${operation} Completed`);
  console.log(`\nTask: ${taskInfo.name}`);
  console.log(`Location: ${taskInfo.location}`);
  
  const response = await askUser("📊 View task dashboard? (yes/no)");
  
  if (response.match(/^(yes|y|open|dashboard)$/i)) {
    await launchDashboard();
    console.log("🌐 Dashboard opened in browser");
  } else {
    console.log("Skipped. Run 'cd dashboard && ./start.sh' anytime.");
  }
}
```

## Auto-Launch Considerations

**Current Design: Opt-in (User must approve)**
- ✅ Respects user control
- ✅ Prevents unexpected browser tabs
- ✅ Works in both interactive and scripted modes

**Alternative: Configurable Auto-launch**
If users want automatic launch, add to config:
```json
{
  "autoOpenDashboard": true
}
```

Then skip the prompt and auto-launch.

## Port Management

Dashboard remembers the last used port in `port.conf`:
- First run: Default 8080
- User specifies: `--port 3000` → saves 3000
- Subsequent runs: Uses saved port automatically

If port is in use, server will error - user should specify a different port.

## Browser Opening

Use platform-appropriate command:
- **macOS**: `open http://localhost:PORT`
- **Linux**: `xdg-open http://localhost:PORT`
- **Windows**: `start http://localhost:PORT`

Or let user manually open the URL.


path: references/maybe-list-workflow.md
content: # Maybe List Operations

Step-by-step workflow for Maybe List management.

## Adding Tasks to Maybe List

### Step 1: Read File
```bash
read: ~/.openclaw/workspace/tasks/maybe.md
```

### Step 2: Find Last Task Number
Scan headers (`#### N. Task Name`) and identify highest number.

### Step 3: Calculate New Number
```
New Number = Last Number + 1
```

### Step 4: Append to End
Add after last existing task. Never insert in middle.

### Step 5: Update Metadata
- Update total count in header
- Update `Last Updated` timestamp

### Step 6: Verify
- No duplicate numbers
- No gaps in sequence
- Last number equals total count

## Example

**Before (25 tasks):**
```markdown
#### 24. Task A
#### 25. Task B
```

**After adding Task C:**
```markdown
#### 24. Task A
#### 25. Task B
#### 26. Task C
```

## Critical Warning

If you see numbering like:
```markdown
#### 16. Task A
#### 26. Task B  <- WRONG! Gap present
#### 17. Task C
```

STOP immediately and renumber ALL tasks sequentially.

## Maybe List Numbering Rules

1. Sequential: 1, 2, 3, 4...
2. Always append to END
3. Never insert in middle with out-of-sequence numbers
4. Verify: last_task_number == total_task_count
5. If discrepancy: renumber ALL tasks


path: references/numbering-rules.md
content: # Task Numbering Rules

Complete numbering rules for all task lists.

## Overview

| File | Numbering Type | Key Rule |
|------|----------------|----------|
| tasks.md | Quadrant-continuous | Q1:1-N, Q2:N+1-M, Q3:M+1-P, Q4:P+1-Q |
| maybe.md | Simple sequential | 1, 2, 3... append to END |
| customer-projects.md | Per-customer | 1, 2, 3... per customer |
| delegation.md | Simple sequential | 1, 2, 3... append to END |
| archived.md | Simple sequential | 1, 2, 3... append to END |

## tasks.md (Quadrant Tasks)

**Rule**: Continuous numbering across quadrants

```
Q1: ### 1. Task A
      ### 2. Task B
Q2: ### 3. Task C  <- Q1_count + 1
      ### 4. Task D
Q3: ### 5. Task E  <- Q2_end + 1
Q4: ### 6. Task F  <- Q3_end + 1
```

**Verification**:
- Q2 starts at Q1_count + 1
- Q3 starts at Q2_end + 1
- Q4 starts at Q3_end + 1
- No gaps between quadrants

## maybe.md (Maybe List)

**Rule**: Sequential 1-N, always append to end

```markdown
#### 24. Task A
#### 25. Task B
#### 26. Task C  <- New task at END
```

**Never do this**:
```markdown
#### 16. Task A
#### 26. Task B  <- WRONG! Gap in middle
#### 17. Task C
```

**Verification**: last_number == total_count

## customer-projects.md (Customer Projects)

**Rule**: Per-customer numbering, independent across customers

```markdown
### Customer A
#### 1. Project X
#### 2. Project Y

### Customer B
#### 1. Project Z  <- Independent numbering
#### 2. Project W
```

**Verification**: Each customer's tasks numbered 1-N independently

## delegation.md (Delegation List)

**Rule**: Sequential 1-N, always append to end

```markdown
### 1. Task A @john
### 2. Task B @mary
### 3. Task C @peter  <- New task at END
```

**Format**: Include assignee: `### X. Task Name @assignee`

**Verification**: last_number == total_count

## archived.md (Archive)

**Rule**: Sequential 1-N, group by archive date

```markdown
## Archived 2026-04-15
### 1. Task A [Completed]
### 2. Task B [Deleted]

## Archived 2026-04-14
### 3. Task C [Completed]
```

**Format**: Include archive date: `### X. Task Name [Archived: YYYY-MM-DD]`

**Verification**: last_number == total_count

## Universal Numbering Checklist

After EVERY operation on ANY list:

- [ ] Read the file
- [ ] Scan all task numbers
- [ ] Check for duplicates
- [ ] Check for gaps
- [ ] Verify sequential from 1
- [ ] Fix issues if found
- [ ] Re-read to confirm

**Trigger auto-fix on phrases**:
- "check task numbering"
- "fix sequence"
- "序号问题"


path: references/output-examples.md
content: # Output Format Examples

Detailed examples for operation output formats.

## After Adding a Task

```markdown
✅ Task Added Successfully

**Summary:**
- Operation: Add new task
- Task: #18 - Example Task Name
- Location: Q2 (Important + Not Urgent)
- Priority: P1

**Statistics:**
| Quadrant | Count | Task Numbers |
|----------|-------|--------------|
| Q1 | 4 | 1-4 |
| Q2 | 14 | 5-18 |
| Q3 | 4 | 19-22 |
| Q4 | 0 | - |
| Total | 22 | - |
```

## After Adding to Customer Projects

```markdown
✅ Customer Project Task Added

**Summary:**
- Operation: Add customer project sub-task
- Customer: Customer Name
- Task: Task Name Description
- Status: Active

**Customer Project Summary:**
| Metric | Value |
|--------|-------|
| Total Sub-tasks | 4 |
| Active | 3 |
| Blocked | 1 |

**Next Step:** Consider adding to Q2 if personal execution required.
```

## After Completing a Task

```markdown
✅ Task Completed

**Summary:**
- Operation: Complete task
- Task: #5 - Example Completed Task
- Moved to: Archive

**Statistics:**
| Quadrant | Before | After |
|----------|--------|-------|
| Q2 | 14 | 13 |
| Completed | - | +1 |

Great progress!
```

## After Adding to Maybe List

```markdown
✅ Maybe List Task Added

**Summary:**
- Operation: Add to Maybe List
- Task: #26 - Example Future Task
- Location: Maybe List

**Statistics:**
| Metric | Value |
|--------|-------|
| Total Maybe Tasks | 26 |
| Last Number | 26 |

Status: Sequential numbering verified.
```


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)