# Action Board Design: "What should I be doing?"

**Version:** 1.0  
**Date:** 2026-02-10  
**Goal:** Show actionable tasks prioritized by due date and dependency state, not project stage

---

## 1. Overview

The Action Board is a **derived view** from the existing dashboard/kanban data model. It reframes tasks from "where they are in the pipeline" (To Do/Doing/Waiting) to "what I should focus on right now" based on urgency, dependencies, and effort.

**Key Philosophy:**
- **Low maintenance:** Auto-generated from existing `dashboard/data.json`
- **Action-oriented:** Every column represents a clear decision or action state
- **Minimal manual work:** Uses existing fields + simple metadata additions
- **Same hosting:** Tailscale Serve, same security model as dashboard/kanban

---

## 2. UI/UX Design

### 2.1 Column Structure

| Column ID | Column Name | Purpose | Sorting |
|-----------|-------------|---------|---------|
| `overdue` | 🔥 **Overdue** | Tasks past due date | Due date (oldest first) |
| `urgent` | ⚡ **Due Soon** | Due within 3 days | Due date (soonest first) |
| `do_now` | ✅ **Do Now** | Unblocked, actionable, due ≤7 days OR high priority with next actions owned by you | Priority (High→Low), then due date |
| `waiting` | ⏳ **Waiting on Others** | Blocked by external dependencies | Priority, then due date |
| `quick_wins` | 🎯 **Quick Wins** | Low effort, unblocked, medium-high value | Priority (High→Low) |
| `someday` | 💭 **Someday/Maybe** | No due date, low priority, or no immediate next action | Area, then name |
| `done_recent` | ✨ **Done Recently** | Completed in last 7 days (for visibility) | Completion date (newest first) |

### 2.2 Visual Design

**Header:**
```
What should I be doing? | Last updated: [timestamp] | [Refresh button]
```

**Card Appearance:**
- **Bold title** with area badge (e.g., `[RPA]`, `[AV]`, `[Personal]`)
- **Due date** prominently displayed (color-coded: red if overdue, orange if ≤3 days, yellow if ≤7 days)
- **First blocker** shown (if any) with icon: `🚧 Dependency: ...`
- **First 2 next actions** (truncated if long)
- **Quick indicators:** 
  - 🔴 High priority
  - 🟡 Medium priority
  - ⚡ Quick win eligible
  - 👤 Owner: You vs Others

**Responsive:**
- Desktop: 3-4 columns wide (grid layout)
- Mobile: Single column, collapsible sections

---

## 3. Data Schema Changes

### 3.1 New Fields in `dashboard/data.json` (optional, inferred if missing)

```json
{
  "id": "p1",
  "name": "Project name",
  "area": "RPA",
  "status": "Pending business sponsor alignment",
  "priority": "High",
  "due": "2026-02-15",
  "nextActions": [...],
  "blockers": [...],
  "notes": "...",
  
  // NEW OPTIONAL FIELDS:
  "estimatedEffort": "quick",  // "quick" (≤30min), "short" (≤2h), "medium" (≤1 day), "long" (>1 day)
  "actionOwner": "self",       // "self" (you), "other" (external), "mixed" (both)
  "completedAt": "2026-02-10T14:30:00-05:00",  // ISO timestamp for done tasks
  "tags": ["remote-access", "production"]      // Optional tags for filtering
}
```

**Backward Compatibility:** All new fields are **optional**. If missing, defaults are inferred:
- `estimatedEffort`: Inferred from next actions count (1-2 = quick, 3-5 = short, 6+ = medium)
- `actionOwner`: Parsed from next actions text (if contains "Owner: Adner" = self, else mixed)
- `completedAt`: Null unless manually set
- `tags`: Empty array

### 3.2 New State File: `action/action-state.json`

Stores manual overrides and preferences:

```json
{
  "meta": {
    "title": "Action Board",
    "updatedAt": "2026-02-10T20:30:00-05:00",
    "timezone": "America/New_York"
  },
  "preferences": {
    "urgentDaysThreshold": 3,     // Days for "Due Soon"
    "doNowDaysThreshold": 7,      // Days for "Do Now"
    "doneRecentDays": 7,          // Days to show in "Done Recently"
    "quickWinMaxActions": 3       // Max next actions to qualify as quick win
  },
  "manualOverrides": {
    "p1": {
      "column": "waiting",        // Force project p1 into Waiting column
      "hidden": false             // Hide from action board entirely
    }
  },
  "dismissedProjects": ["p4"]     // Temporarily hide (e.g., vacation planning)
}
```

---

## 4. Derivation Algorithm

### 4.1 Column Assignment Logic (Priority Order)

For each project in `dashboard/data.json`:

```python
def assign_action_column(project, prefs, overrides):
    """
    Assign project to action board column.
    Returns: (column_id, sort_key)
    """
    pid = project.get('id')
    
    # 1. Check manual override
    if pid in overrides and overrides[pid].get('column'):
        return (overrides[pid]['column'], get_sort_key(project, overrides[pid]['column']))
    
    # 2. Check if dismissed
    if pid in prefs.get('dismissedProjects', []):
        return (None, None)  # Don't show
    
    # 3. Check completion
    if project.get('completedAt'):
        completed_date = parse_iso(project['completedAt'])
        if days_ago(completed_date) <= prefs['doneRecentDays']:
            return ('done_recent', completed_date)
        else:
            return (None, None)  # Too old, hide
    
    # 4. Parse due date
    due = parse_due_date(project.get('due'))
    days_until_due = (due - today()).days if due else None
    
    # 5. Check overdue
    if days_until_due is not None and days_until_due < 0:
        return ('overdue', due)
    
    # 6. Check urgent (due soon)
    if days_until_due is not None and days_until_due <= prefs['urgentDaysThreshold']:
        return ('urgent', due)
    
    # 7. Check blockers (waiting on others)
    blockers = project.get('blockers', [])
    if len(blockers) > 0:
        return ('waiting', (project.get('priority'), due))
    
    # 8. Check quick wins
    next_actions = project.get('nextActions', [])
    effort = infer_effort(project)
    priority = project.get('priority', 'Low')
    is_quick = effort == 'quick' and len(next_actions) <= prefs['quickWinMaxActions']
    is_valuable = priority in ['High', 'Medium']
    
    if is_quick and is_valuable and len(next_actions) > 0:
        return ('quick_wins', (priority_rank(priority), due))
    
    # 9. Check "Do Now" (actionable + due soon or high priority with actions)
    action_owner = infer_action_owner(project)
    has_actions = len(next_actions) > 0 and action_owner in ['self', 'mixed']
    
    if has_actions:
        if days_until_due and days_until_due <= prefs['doNowDaysThreshold']:
            return ('do_now', (priority_rank(priority), due))
        elif priority == 'High':
            return ('do_now', (priority_rank(priority), due))
    
    # 10. Default: Someday/Maybe
    return ('someday', (project.get('area'), project.get('name')))


def infer_effort(project):
    """Infer effort from estimatedEffort field or next actions count."""
    if project.get('estimatedEffort'):
        return project['estimatedEffort']
    
    action_count = len(project.get('nextActions', []))
    if action_count <= 2:
        return 'quick'
    elif action_count <= 5:
        return 'short'
    elif action_count <= 10:
        return 'medium'
    else:
        return 'long'


def infer_action_owner(project):
    """Infer who owns next actions: self, other, or mixed."""
    if project.get('actionOwner'):
        return project['actionOwner']
    
    actions = project.get('nextActions', [])
    has_self = any('Owner: Adner' in a or 'Owner: MrAnderson' in a for a in actions)
    has_other = any('Owner:' in a and 'Owner: Adner' not in a and 'Owner: MrAnderson' not in a for a in actions)
    
    if has_self and not has_other:
        return 'self'
    elif has_other and not has_self:
        return 'other'
    elif has_self and has_other:
        return 'mixed'
    else:
        return 'self'  # Default: assume user owns it


def parse_due_date(due_str):
    """Parse due date (supports 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM TZ')."""
    if not due_str:
        return None
    # Handle both date-only and datetime formats
    # Return datetime object (naive or aware)
    ...


def priority_rank(priority):
    """Convert priority to sortable rank (lower = higher priority)."""
    return {'High': 0, 'Medium': 1, 'Low': 2}.get(priority, 3)
```

### 4.2 Filtering Rules

**Hide from Action Board if:**
- Project has `completedAt` older than `doneRecentDays` (default 7 days)
- Project ID is in `dismissedProjects` list
- Project has no `nextActions`, no `due date`, and priority is `Low` (auto-categorize as noise)

**Exception:** Always show if:
- Has a due date (even if no actions)
- Has blockers (visibility for follow-up)
- Priority is High (even if no due date)

---

## 5. Implementation Steps

### 5.1 File Structure

```
workspace/
├── action/
│   ├── index.html              # Action board UI
│   ├── action-state.json       # Preferences + overrides
│   ├── data.json               # Generated action board data
│   └── build-action-data.py    # Build script
├── dashboard/
│   └── data.json               # Source of truth
├── kanban/
│   └── ...
└── serve.py                    # HTTP server (add /action/ route)
```

### 5.2 Build Script: `action/build-action-data.py`

```python
#!/usr/bin/env python3
"""Generate action board data from dashboard projects."""
import json
from datetime import datetime, timedelta
from pathlib import Path

ROOT = Path('/home/isthekid/.openclaw/workspace')
DASH = ROOT / 'dashboard' / 'data.json'
STATE = ROOT / 'action' / 'action-state.json'
OUT = ROOT / 'action' / 'data.json'

# Default preferences
DEFAULT_PREFS = {
    'urgentDaysThreshold': 3,
    'doNowDaysThreshold': 7,
    'doneRecentDays': 7,
    'quickWinMaxActions': 3,
}

def main():
    # Load data
    dash = json.loads(DASH.read_text())
    
    # Load or create state
    if STATE.exists():
        state = json.loads(STATE.read_text())
    else:
        state = {
            'meta': {'title': 'Action Board', 'timezone': 'America/New_York'},
            'preferences': DEFAULT_PREFS,
            'manualOverrides': {},
            'dismissedProjects': []
        }
        STATE.parent.mkdir(exist_ok=True)
        STATE.write_text(json.dumps(state, indent=2))
    
    prefs = {**DEFAULT_PREFS, **state.get('preferences', {})}
    overrides = state.get('manualOverrides', {})
    dismissed = state.get('dismissedProjects', [])
    
    # Define columns
    columns = [
        {'id': 'overdue', 'name': '🔥 Overdue', 'cards': []},
        {'id': 'urgent', 'name': '⚡ Due Soon', 'cards': []},
        {'id': 'do_now', 'name': '✅ Do Now', 'cards': []},
        {'id': 'waiting', 'name': '⏳ Waiting on Others', 'cards': []},
        {'id': 'quick_wins', 'name': '🎯 Quick Wins', 'cards': []},
        {'id': 'someday', 'name': '💭 Someday/Maybe', 'cards': []},
        {'id': 'done_recent', 'name': '✨ Done Recently', 'cards': []},
    ]
    col_map = {c['id']: c for c in columns}
    
    # Process projects
    projects = dash.get('projects', [])
    today = datetime.now().date()
    
    for project in projects:
        pid = project.get('id')
        if not pid or pid in dismissed:
            continue
        
        # Assign column
        column_id, sort_key = assign_action_column(project, prefs, overrides, today)
        
        if not column_id:
            continue  # Hidden
        
        # Create card
        card = project_to_action_card(project, prefs, today)
        card['_sortKey'] = sort_key
        
        col_map[column_id]['cards'].append(card)
    
    # Sort cards within columns
    for col in columns:
        col['cards'].sort(key=lambda c: c.pop('_sortKey', (0, 0)))
    
    # Generate output
    output = {
        'meta': {
            'title': state.get('meta', {}).get('title', 'Action Board'),
            'updatedAt': datetime.now().astimezone().isoformat(timespec='seconds'),
            'timezone': dash.get('meta', {}).get('timezone', 'America/New_York'),
        },
        'columns': columns,
        'preferences': prefs,
    }
    
    OUT.write_text(json.dumps(output, indent=2))
    
    # Embed into index.html (like kanban)
    embed_into_html()


def assign_action_column(project, prefs, overrides, today):
    """Assign project to action board column. Returns (column_id, sort_key)."""
    # (Implement logic from section 4.1 above)
    ...


def project_to_action_card(project, prefs, today):
    """Convert project to action card with relevant fields."""
    return {
        'id': project.get('id'),
        'title': project.get('name'),
        'area': project.get('area'),
        'priority': project.get('priority', 'Low'),
        'due': project.get('due'),
        'daysUntilDue': calculate_days_until_due(project.get('due'), today),
        'nextActions': project.get('nextActions', [])[:2],  # First 2 only
        'blockers': project.get('blockers', [])[:1],        # First blocker only
        'effort': infer_effort(project),
        'actionOwner': infer_action_owner(project),
        'status': project.get('status'),
        'tags': project.get('tags', []),
    }


def embed_into_html():
    """Embed data.json into index.html for offline/file:// access."""
    idx = ROOT / 'action' / 'index.html'
    data = json.loads(OUT.read_text())
    json_text = json.dumps(data, indent=2)
    
    if not idx.exists():
        return  # Create HTML first
    
    html = idx.read_text()
    start = '<!--DATA_START-->'
    end = '<!--DATA_END-->'
    
    if start in html and end in html:
        pre, rest = html.split(start, 1)
        _, post = rest.split(end, 1)
        idx.write_text(pre + start + json_text + end + post)


if __name__ == '__main__':
    main()
```

### 5.3 HTML Template: `action/index.html`

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Action Board</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      min-height: 100vh;
      padding: 20px;
    }
    .container {
      max-width: 1600px;
      margin: 0 auto;
    }
    header {
      background: white;
      border-radius: 12px;
      padding: 20px 30px;
      margin-bottom: 20px;
      box-shadow: 0 4px 6px rgba(0,0,0,0.1);
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    h1 { font-size: 28px; color: #333; }
    .meta { font-size: 14px; color: #666; }
    .board {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
      gap: 20px;
    }
    .column {
      background: rgba(255,255,255,0.95);
      border-radius: 12px;
      padding: 16px;
      box-shadow: 0 4px 6px rgba(0,0,0,0.1);
    }
    .column-header {
      font-size: 18px;
      font-weight: 600;
      margin-bottom: 12px;
      padding-bottom: 8px;
      border-bottom: 2px solid #e0e0e0;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .card-count {
      background: #667eea;
      color: white;
      font-size: 12px;
      padding: 2px 8px;
      border-radius: 12px;
    }
    .card {
      background: white;
      border: 1px solid #e0e0e0;
      border-radius: 8px;
      padding: 12px;
      margin-bottom: 12px;
      cursor: pointer;
      transition: all 0.2s;
    }
    .card:hover {
      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
      transform: translateY(-2px);
    }
    .card-title {
      font-size: 15px;
      font-weight: 600;
      margin-bottom: 8px;
      color: #333;
    }
    .card-meta {
      display: flex;
      gap: 8px;
      flex-wrap: wrap;
      margin-bottom: 8px;
    }
    .badge {
      font-size: 11px;
      padding: 3px 8px;
      border-radius: 4px;
      font-weight: 500;
    }
    .badge-area { background: #e3f2fd; color: #1976d2; }
    .badge-priority-high { background: #ffebee; color: #c62828; }
    .badge-priority-medium { background: #fff3e0; color: #ef6c00; }
    .badge-priority-low { background: #f1f8e9; color: #558b2f; }
    .badge-due { background: #fce4ec; color: #880e4f; }
    .badge-due-overdue { background: #f44336; color: white; font-weight: 600; }
    .badge-due-urgent { background: #ff9800; color: white; }
    .badge-due-soon { background: #ffeb3b; color: #333; }
    .card-blocker {
      font-size: 13px;
      color: #d32f2f;
      margin: 6px 0;
      padding: 6px;
      background: #ffebee;
      border-radius: 4px;
    }
    .card-actions {
      font-size: 13px;
      color: #555;
      margin-top: 8px;
    }
    .card-actions li {
      margin-left: 16px;
      margin-bottom: 4px;
    }
    .empty-column {
      text-align: center;
      color: #999;
      padding: 40px 20px;
      font-style: italic;
    }
    @media (max-width: 768px) {
      .board { grid-template-columns: 1fr; }
    }
  </style>
</head>
<body>
  <div class="container">
    <header>
      <div>
        <h1 id="board-title">What should I be doing?</h1>
        <div class="meta">Last updated: <span id="updated-at">—</span></div>
      </div>
      <button onclick="location.reload()" style="padding: 10px 20px; cursor: pointer;">Refresh</button>
    </header>
    
    <div class="board" id="board"></div>
  </div>

  <script>
    // Embedded data fallback for file:// protocol
    const embeddedData = <!--DATA_START-->null<!--DATA_END-->;
    
    async function loadData() {
      try {
        const resp = await fetch('data.json');
        return await resp.json();
      } catch (e) {
        return embeddedData;
      }
    }
    
    function renderBoard(data) {
      document.getElementById('updated-at').textContent = data.meta.updatedAt;
      
      const board = document.getElementById('board');
      board.innerHTML = '';
      
      data.columns.forEach(col => {
        const colEl = document.createElement('div');
        colEl.className = 'column';
        
        const header = document.createElement('div');
        header.className = 'column-header';
        header.innerHTML = `
          <span>${col.name}</span>
          <span class="card-count">${col.cards.length}</span>
        `;
        colEl.appendChild(header);
        
        if (col.cards.length === 0) {
          const empty = document.createElement('div');
          empty.className = 'empty-column';
          empty.textContent = 'Nothing here!';
          colEl.appendChild(empty);
        } else {
          col.cards.forEach(card => {
            colEl.appendChild(renderCard(card));
          });
        }
        
        board.appendChild(colEl);
      });
    }
    
    function renderCard(card) {
      const cardEl = document.createElement('div');
      cardEl.className = 'card';
      
      // Title
      const title = document.createElement('div');
      title.className = 'card-title';
      title.textContent = card.title;
      cardEl.appendChild(title);
      
      // Meta badges
      const meta = document.createElement('div');
      meta.className = 'card-meta';
      
      if (card.area) {
        meta.innerHTML += `<span class="badge badge-area">${card.area}</span>`;
      }
      
      if (card.priority) {
        const priorityClass = `badge-priority-${card.priority.toLowerCase()}`;
        meta.innerHTML += `<span class="badge ${priorityClass}">${card.priority}</span>`;
      }
      
      if (card.due) {
        const dueBadge = getDueBadge(card.daysUntilDue, card.due);
        meta.innerHTML += dueBadge;
      }
      
      if (card.effort) {
        const effortEmoji = {'quick': '⚡', 'short': '⏱️', 'medium': '📅', 'long': '📆'}[card.effort] || '';
        meta.innerHTML += `<span class="badge" style="background: #f5f5f5;">${effortEmoji} ${card.effort}</span>`;
      }
      
      cardEl.appendChild(meta);
      
      // Blocker
      if (card.blockers && card.blockers.length > 0) {
        const blocker = document.createElement('div');
        blocker.className = 'card-blocker';
        blocker.textContent = `🚧 ${card.blockers[0]}`;
        cardEl.appendChild(blocker);
      }
      
      // Next actions
      if (card.nextActions && card.nextActions.length > 0) {
        const actions = document.createElement('div');
        actions.className = 'card-actions';
        actions.innerHTML = '<ul>' + card.nextActions.map(a => `<li>${truncate(a, 80)}</li>`).join('') + '</ul>';
        cardEl.appendChild(actions);
      }
      
      return cardEl;
    }
    
    function getDueBadge(daysUntil, dueStr) {
      if (daysUntil < 0) {
        return `<span class="badge badge-due-overdue">Overdue ${Math.abs(daysUntil)}d</span>`;
      } else if (daysUntil <= 3) {
        return `<span class="badge badge-due-urgent">Due in ${daysUntil}d</span>`;
      } else if (daysUntil <= 7) {
        return `<span class="badge badge-due-soon">Due ${dueStr.split('T')[0]}</span>`;
      } else {
        return `<span class="badge badge-due">Due ${dueStr.split('T')[0]}</span>`;
      }
    }
    
    function truncate(str, len) {
      return str.length > len ? str.slice(0, len) + '...' : str;
    }
    
    // Load and render
    loadData().then(renderBoard);
  </script>
</body>
</html>
```

### 5.4 Integration Steps

**Step 1: Create action directory and files**
```bash
cd /home/isthekid/.openclaw/workspace
mkdir -p action
```

**Step 2: Implement `build-action-data.py`**
- Copy full implementation from section 5.2 above
- Make executable: `chmod +x action/build-action-data.py`

**Step 3: Create `action/index.html`**
- Copy HTML template from section 5.3

**Step 4: Initialize state file**
```bash
echo '{"meta":{"title":"Action Board","timezone":"America/New_York"},"preferences":{"urgentDaysThreshold":3,"doNowDaysThreshold":7,"doneRecentDays":7,"quickWinMaxActions":3},"manualOverrides":{},"dismissedProjects":[]}' > action/action-state.json
```

**Step 5: Update `watch-and-rebuild.sh` to include action board**
```bash
#!/bin/bash
# Add to existing watch script
while true; do
  inotifywait -e modify,create,delete dashboard/data.json kanban/kanban-state.json action/action-state.json
  echo "Change detected, rebuilding..."
  ./dashboard/build-dashboard-embed.py
  ./kanban/build-kanban-data.py
  ./action/build-action-data.py  # NEW
  echo "Rebuilt at $(date)"
  sleep 1
done
```

**Step 6: Test build**
```bash
cd /home/isthekid/.openclaw/workspace
./action/build-action-data.py
ls -lh action/data.json  # Should exist now
```

**Step 7: Update `serve.py` (already supports subdirectories, no changes needed)**

**Step 8: Access via Tailscale Serve**
- Action board will be available at: `http://localhost:8080/action/`
- Or via Tailscale: `https://<tailscale-name>.tailnet/action/`

---

## 6. Maintenance Strategy

### 6.1 Zero-Manual-Work Design

**Principle:** The action board should require **no ongoing manual updates** beyond maintaining the source `dashboard/data.json`.

**How it works:**
1. **Dashboard is source of truth:** Update projects in `dashboard/data.json` (same as current workflow)
2. **Automatic derivation:** Run `./action/build-action-data.py` (or watch script auto-runs)
3. **Action board auto-updates:** Columns, cards, sorting all derived algorithmically

**No manual column placement:** Unlike the kanban board (which uses `kanban-state.json` to manually place cards), the action board is **fully automatic**.

### 6.2 Optional Manual Controls

For rare cases where automatic placement is wrong:

**1. Override column placement:**
Edit `action/action-state.json`:
```json
{
  "manualOverrides": {
    "p4": {
      "column": "someday",  // Force vacation planning to Someday
      "hidden": false
    }
  }
}
```

**2. Temporarily dismiss projects:**
```json
{
  "dismissedProjects": ["p4", "p9"]  // Hide vacation + travel planning
}
```

**3. Adjust thresholds:**
```json
{
  "preferences": {
    "urgentDaysThreshold": 5,  // Change "Due Soon" from 3 to 5 days
    "doNowDaysThreshold": 10,
    "quickWinMaxActions": 2
  }
}
```

### 6.3 Periodic Review

**Weekly (automated via cron or heartbeat):**
- Check for stale "Waiting on Others" (>14 days) → notify
- Identify projects in "Someday" with approaching due dates → alert

**Monthly:**
- Review `dismissedProjects` and clear old ones
- Archive completed projects from `dashboard/data.json` to `dashboard/archive.json`

---

## 7. Security & Hosting

**Same model as dashboard/kanban:**
- Served via `serve.py` on `localhost:8080`
- Exposed via **Tailscale Serve** (HTTPS, authenticated)
- No public exposure, no additional auth needed
- Data stays local, no external dependencies

**Access URL (example):**
```
https://adner-desktop.tailnet-name.ts.net/action/
```

**No changes to hosting infrastructure required.**

---

## 8. Advanced Features (Future Enhancements)

### 8.1 Smart Suggestions

**Add "Suggested Focus" section at top:**
- "Based on your calendar, you have 2 hours free today. Recommended: Quick Wins."
- "You have 3 overdue tasks. Start with: [top overdue task]"

**Implementation:** Add `suggestions` array to `data.json`, derive from calendar API + current state.

### 8.2 Time Estimates

**Show total time for each column:**
- "Do Now: ~4.5 hours of work"
- "Quick Wins: ~1 hour total"

**Implementation:** Add `estimatedTime` field to projects, sum per column.

### 8.3 Integration with Calendar

**Block time automatically:**
- Click "Schedule" button on card → creates calendar block for next available slot
- Requires calendar API access (already have Google Calendar skill)

### 8.4 Completion Tracking

**Weekly summary:**
- "This week you completed 5 tasks, cleared 2 blockers, avg 3.2 days to completion"
- Requires storing historical completion data

### 8.5 Mobile Shortcuts

**iOS Shortcuts integration:**
- "What should I do next?" → opens action board, reads top 3 Do Now items via TTS
- "Mark [project] done" → updates dashboard, rebuilds board

---

## 9. Testing Plan

### 9.1 Test Scenarios

| Scenario | Expected Column | Test Data |
|----------|-----------------|-----------|
| Task due tomorrow, unblocked, high priority | Urgent | `due: 2026-02-11, priority: High, blockers: []` |
| Task due in 5 days, 2 actions, owned by you | Do Now | `due: 2026-02-15, nextActions: [2], actionOwner: self` |
| Task blocked, due next week | Waiting on Others | `due: 2026-02-17, blockers: ["Pending USIS quote"]` |
| Task with 1 action, medium priority, no due date | Quick Wins | `nextActions: [1], priority: Medium, estimatedEffort: quick` |
| Task completed 3 days ago | Done Recently | `completedAt: 2026-02-07` |
| Task with no due date, low priority, no actions | Someday/Maybe | `due: null, priority: Low, nextActions: []` |
| Task overdue by 2 days | Overdue | `due: 2026-02-08` |

### 9.2 Validation Steps

1. **Create test data:** Add test projects to `dashboard/data.json`
2. **Run build script:** `./action/build-action-data.py`
3. **Verify output:** Check `action/data.json` for correct column assignments
4. **Visual test:** Open `http://localhost:8080/action/` and verify UI
5. **Edge cases:**
   - Project with no ID (should be skipped)
   - Project with invalid due date (should handle gracefully)
   - Project dismissed in state file (should not appear)

---

## 10. Migration Path

**Phase 1: Parallel Run (Week 1)**
- Deploy action board alongside existing dashboard/kanban
- Use for reference only, don't replace existing workflow
- Gather feedback on column logic and sorting

**Phase 2: Refinement (Week 2)**
- Adjust thresholds based on usage
- Add manual overrides for edge cases
- Fix any bugs in derivation logic

**Phase 3: Primary Use (Week 3+)**
- Make action board default landing page
- Dashboard becomes "project details view"
- Kanban becomes "manual workflow override"

**Rollback:** If action board doesn't work, just remove `/action/` directory. No impact on dashboard/kanban.

---

## 11. Code Deliverables Summary

**New Files:**
1. `action/build-action-data.py` (build script)
2. `action/index.html` (UI)
3. `action/action-state.json` (preferences + overrides)
4. `action/data.json` (generated output)

**Modified Files:**
1. `watch-and-rebuild.sh` (add action build step)

**No changes required:**
- `serve.py` (already serves subdirectories)
- `dashboard/data.json` (source of truth, unchanged)
- Security/hosting (same Tailscale Serve setup)

**Total implementation effort:** ~4-6 hours for initial version, ~2 hours for refinement.

---

## 12. Example Output

**Sample `action/data.json` (abbreviated):**

```json
{
  "meta": {
    "title": "Action Board",
    "updatedAt": "2026-02-10T20:30:00-05:00",
    "timezone": "America/New_York"
  },
  "columns": [
    {
      "id": "overdue",
      "name": "🔥 Overdue",
      "cards": [
        {
          "id": "p4",
          "title": "DR Vacation Planning (Feb 16–20 flexible)",
          "area": "Personal",
          "priority": "Low",
          "due": "2026-02-10",
          "daysUntilDue": 0,
          "nextActions": [
            "Pick travel window (Feb 13/15 depart, Feb 20/21 return)",
            "Choose area: Bayahibe via PUJ transfer vs Punta Cana"
          ],
          "blockers": [],
          "effort": "short",
          "actionOwner": "self"
        }
      ]
    },
    {
      "id": "urgent",
      "name": "⚡ Due Soon",
      "cards": [
        {
          "id": "p6",
          "title": "Presidents Day voicemail OOO recording",
          "area": "Telecom",
          "priority": "High",
          "due": "2026-02-11 13:00 ET",
          "daysUntilDue": 1,
          "nextActions": [
            "Find the audio files Sandy sent (attachment/link) and confirm they are the correct OOO recording",
            "Upload/apply the OOO voicemail recording for the voicemail boxes"
          ],
          "blockers": [
            "Need the audio files Sandy sent (attachment or location)"
          ],
          "effort": "quick",
          "actionOwner": "self"
        }
      ]
    },
    {
      "id": "do_now",
      "name": "✅ Do Now",
      "cards": [
        {
          "id": "p3",
          "title": "Distribution list access cleanup (MS Dynamics)",
          "area": "Access Governance",
          "priority": "High",
          "due": null,
          "daysUntilDue": null,
          "nextActions": [
            "Send follow-up to Jerry Laroche + Tiffany to confirm companies to remove from MS Dynamics",
            "Send note to Jennifer Freise to request steps/runbook for removing groups from MS Dynamics"
          ],
          "blockers": [],
          "effort": "quick",
          "actionOwner": "self"
        }
      ]
    },
    {
      "id": "waiting",
      "name": "⏳ Waiting on Others",
      "cards": [
        {
          "id": "p1",
          "title": "Blue Prism RPA POC (BINC)",
          "area": "RPA",
          "priority": "High",
          "due": null,
          "daysUntilDue": null,
          "nextActions": [
            "Await Erika Kirchner confirmation/alignment (scope, cost, timing) before any execution"
          ],
          "blockers": [
            "Dependency: HR alignment with Erika Kirchner"
          ],
          "effort": "short",
          "actionOwner": "other"
        }
      ]
    },
    {
      "id": "quick_wins",
      "name": "🎯 Quick Wins",
      "cards": []
    },
    {
      "id": "someday",
      "name": "💭 Someday/Maybe",
      "cards": [
        {
          "id": "p2",
          "title": "Gmail Intake Automation (srvdeskops@gmail.com)",
          "area": "Automation",
          "priority": "Medium",
          "due": null,
          "daysUntilDue": null,
          "nextActions": [
            "Tailscale remote access preflight: confirm Tailscale installed and logged in on OpenClaw machine and phone/laptop",
            "Decide access mode: Tailnet-only (private) vs Tailscale Serve HTTPS (recommended)"
          ],
          "blockers": [],
          "effort": "medium",
          "actionOwner": "self",
          "status": "Stable"
        }
      ]
    },
    {
      "id": "done_recent",
      "name": "✨ Done Recently",
      "cards": []
    }
  ],
  "preferences": {
    "urgentDaysThreshold": 3,
    "doNowDaysThreshold": 7,
    "doneRecentDays": 7,
    "quickWinMaxActions": 3
  }
}
```

---

## 13. FAQ

**Q: Why not just use the kanban board?**  
A: Kanban shows *where* tasks are in the workflow (To Do/Doing/Waiting). Action board shows *what* to work on *right now* based on urgency and dependencies. Different mental models for different purposes.

**Q: Do I need to maintain both kanban and action board?**  
A: No. The action board is **auto-generated** from `dashboard/data.json`. You only maintain one source: the dashboard. Kanban and action board are both derived views.

**Q: What if I want to hide a project from the action board but keep it in the dashboard?**  
A: Add it to `dismissedProjects` in `action-state.json`. It will still appear in dashboard/kanban but not in the action board.

**Q: Can I manually move cards between columns?**  
A: Yes, using `manualOverrides` in `action-state.json`. But the goal is to avoid manual work—adjust the algorithm instead if placement is wrong.

**Q: How do I mark a task as done?**  
A: Add `"completedAt": "2026-02-10T15:30:00-05:00"` to the project in `dashboard/data.json`, then rebuild. It will move to "Done Recently" for 7 days, then disappear.

**Q: What about recurring tasks?**  
A: Not in v1. Future enhancement: add `recurrence` field (e.g., `"recurrence": "weekly"`) and auto-create new instances after completion.

**Q: Can I access this on mobile?**  
A: Yes! The UI is responsive. Access via Tailscale URL on your phone browser. Future: iOS Shortcuts integration for voice commands.

---

## 14. Conclusion

The Action Board transforms your project data into a **decision-making tool**: instead of "where are my projects?" it answers "**what should I be doing right now?**"

**Key Benefits:**
- ✅ **Zero maintenance:** Auto-generated from existing dashboard
- ✅ **Actionable:** Every column represents a clear decision state
- ✅ **Context-aware:** Prioritizes by due date, dependencies, and effort
- ✅ **Minimal overhead:** Same hosting, same security, same workflow
- ✅ **Flexible:** Adjust thresholds and overrides without code changes

**Next Steps:**
1. Implement `build-action-data.py` (section 5.2)
2. Create `index.html` (section 5.3)
3. Test with existing dashboard data
4. Expose via Tailscale Serve
5. Use for 1-2 weeks and refine

**Estimated Timeline:**
- **Day 1:** Implement build script + HTML template (4 hours)
- **Day 2:** Test, refine algorithm, fix bugs (2 hours)
- **Week 1-2:** Parallel run with feedback
- **Week 3+:** Primary workflow

**Success Criteria:**
- Action board accurately reflects "what to do next" without manual updates
- Reduces decision fatigue: no more "where do I start?"
- Maintains itself: works with existing update workflow

**Ready to implement?** Start with `build-action-data.py` and test with your current 9 projects. The algorithm should correctly place them into columns based on the logic in section 4.1.

---

*End of Design Document*
