Files
trading_bot_v4/scripts/sync-roadmap-to-deck.py
mindesbunister 6dbbe3ea57 feat: add granular phase-level cards to Nextcloud Deck sync
- Updated parser to extract phases from detailed roadmap files
- Cleaner card titles: 'Phase X: Description' instead of file paths
- Improved status detection: CURRENT/DEPLOYED → In Progress, NEXT → Planning
- Code block removal to prevent API 400/500 errors
- Shorter descriptions (400 chars max) for better readability
- All 21 cards created: 3 initiatives + 18 phases

Card distribution:
- Backlog: 6 cards (future work)
- Planning: 1 card (next phase)
- In Progress: 10 cards (active work)
- Complete: 4 cards (done)

Changes:
- scripts/sync-roadmap-to-deck.py: Complete parser rewrite for phase-level granularity
- Handles both ## Phase and ### Phase patterns
- Removes markdown/emojis from titles for clean display
2025-11-14 11:39:03 +01:00

357 lines
13 KiB
Python
Executable File
Raw Blame History

#!/usr/bin/env python3
"""
Nextcloud Deck <-> Trading Bot Roadmap Sync
Syncs roadmap markdown files with Nextcloud Deck kanban board
"""
import json
import re
import sys
import os
import requests
from pathlib import Path
from typing import Dict, List, Optional
from datetime import datetime, timedelta
import argparse
# Configuration
DECK_CONFIG_FILE = '/tmp/deck-config.json'
ROADMAP_DIR = Path('/home/icke/traderv4')
# Load Nextcloud Deck config
with open(DECK_CONFIG_FILE, 'r') as f:
DECK_CONFIG = json.load(f)
NEXTCLOUD_URL = DECK_CONFIG['url']
NEXTCLOUD_USER = DECK_CONFIG['user']
NEXTCLOUD_PASSWORD = DECK_CONFIG['password']
BOARD_ID = DECK_CONFIG['board_id']
# Stack mapping
STACKS = {stack['name']: stack['id'] for stack in DECK_CONFIG['stacks']}
# Status to stack mapping
STATUS_TO_STACK = {
'IN PROGRESS': '🚀 In Progress',
'🔄 IN PROGRESS': '🚀 In Progress',
'COMPLETE': '✅ Complete',
'✅ COMPLETE': '✅ Complete',
'PENDING': '📋 Planning',
'🔜 NEXT': '📋 Planning',
'FUTURE': '📥 Backlog',
'🎯 FUTURE': '📥 Backlog',
}
# Roadmap files to parse
ROADMAP_FILES = [
'OPTIMIZATION_MASTER_ROADMAP.md',
'SIGNAL_QUALITY_OPTIMIZATION_ROADMAP.md',
'POSITION_SCALING_ROADMAP.md',
'ATR_BASED_TP_ROADMAP.md',
]
class DeckAPI:
"""Nextcloud Deck API wrapper"""
def __init__(self):
self.base_url = f"{NEXTCLOUD_URL}/index.php/apps/deck/api/v1.0"
self.auth = (NEXTCLOUD_USER, NEXTCLOUD_PASSWORD)
self.headers = {
'OCS-APIRequest': 'true',
'Content-Type': 'application/json'
}
def get_board_details(self) -> Dict:
"""Get full board details including stacks and cards"""
url = f"{self.base_url}/boards/{BOARD_ID}"
response = requests.get(url, auth=self.auth, headers=self.headers, verify=False)
response.raise_for_status()
return response.json()
def get_cards_in_stack(self, stack_id: int) -> List[Dict]:
"""Get all cards in a specific stack"""
url = f"{self.base_url}/stacks/{stack_id}/cards"
response = requests.get(url, auth=self.auth, headers=self.headers, verify=False)
response.raise_for_status()
return response.json()
def create_card(self, stack_id: int, title: str, description: str,
due_date: Optional[str] = None, labels: Optional[List[str]] = None) -> Dict:
"""Create a new card"""
url = f"{self.base_url}/boards/{BOARD_ID}/stacks/{stack_id}/cards"
payload = {
'title': title,
'type': 'plain',
'order': 999,
'description': description,
}
if due_date:
payload['duedate'] = due_date
response = requests.post(url, auth=self.auth, headers=self.headers,
json=payload, verify=False)
response.raise_for_status()
return response.json()
def update_card(self, card_id: int, **kwargs) -> Dict:
"""Update an existing card"""
url = f"{self.base_url}/boards/{BOARD_ID}/cards/{card_id}"
response = requests.put(url, auth=self.auth, headers=self.headers,
json=kwargs, verify=False)
response.raise_for_status()
return response.json()
def get_all_cards(self) -> List[Dict]:
"""Get all cards from all stacks"""
all_cards = []
for stack_name, stack_id in STACKS.items():
try:
cards = self.get_cards_in_stack(stack_id)
for card in cards:
card['stack_name'] = stack_name
card['stack_id'] = stack_id
all_cards.extend(cards)
except requests.exceptions.HTTPError as e:
# Stack might be empty, continue
print(f"⚠️ Warning: Could not get cards from stack '{stack_name}': {e}")
continue
return all_cards
class RoadmapParser:
"""Parse roadmap markdown files"""
@staticmethod
def parse_file(filepath: Path) -> List[Dict]:
"""Parse a roadmap file and extract phases/tasks"""
if not filepath.exists():
return []
content = filepath.read_text()
filename = filepath.name
tasks = []
# Master roadmap: Parse initiatives (high-level overview only)
if filename == 'OPTIMIZATION_MASTER_ROADMAP.md':
initiative_pattern = r'## (?:🎯|📐|📊) Initiative (\d+): (.+?)(?=\n##|\Z)'
initiatives = re.finditer(initiative_pattern, content, re.DOTALL)
for initiative in initiatives:
initiative_num = initiative.group(1)
initiative_title = initiative.group(2).strip()
initiative_content = initiative.group(0)
# Extract file reference
file_match = re.search(r'\*\*File:\*\* \[`(.+?)`\]', initiative_content)
file_ref = file_match.group(1) if file_match else ''
# Extract purpose
purpose_match = re.search(r'### Purpose\n(.+?)(?=\n###|\Z)', initiative_content, re.DOTALL)
purpose = purpose_match.group(1).strip() if purpose_match else ''
# Extract current status
status_match = re.search(r'### Current Status\n(.+?)(?=\n###|\Z)', initiative_content, re.DOTALL)
status = status_match.group(1).strip() if status_match else ''
# Determine phase status
phase_status = 'FUTURE'
if '✅ COMPLETE' in status or 'COMPLETE' in status:
phase_status = 'COMPLETE'
elif '🔄 IN PROGRESS' in status or 'IN PROGRESS' in status:
phase_status = 'IN PROGRESS'
elif '🔜 NEXT' in status:
phase_status = 'PENDING'
task = {
'title': f"Initiative {initiative_num}: {initiative_title}",
'description': f"""**File:** [{file_ref}](./{file_ref})
{purpose}
**Status:** {status}
""",
'status': phase_status,
'progress': '',
'due_date': None,
'labels': [f'initiative-{initiative_num}'],
}
tasks.append(task)
# Detailed roadmaps: Parse phases (use ## or ### pattern depending on file)
else:
# Try ## Phase pattern first (SIGNAL_QUALITY, POSITION_SCALING)
phase_pattern = r'## Phase (\d+): (.+?)(?=\n(?:##|\Z))'
phases = list(re.finditer(phase_pattern, content, re.DOTALL))
# If no ## Phase found, try ### Phase pattern (ATR_BASED_TP)
if not phases:
phase_pattern = r'### Phase (\d+): (.+?)(?=\n(?:###|##|\Z))'
phases = list(re.finditer(phase_pattern, content, re.DOTALL))
for phase in phases:
phase_num = phase.group(1)
phase_title_raw = phase.group(2).strip()
# Clean up phase title (remove status emojis and extra text in parens)
phase_title = re.sub(r'\s*\(.*?\)\s*', ' ', phase_title_raw)
phase_title = re.sub(r'[🔄✅🔜🎯⏳🤖📏📊🔮]+', '', phase_title).strip()
# Extract full phase content for description
phase_start = phase.end()
next_phase_match = re.search(r'\n(?:##|###) Phase \d+:', content[phase_start:])
phase_end = phase_start + next_phase_match.start() if next_phase_match else len(content)
phase_content = content[phase_start:phase_end].strip()
# Remove code blocks FIRST (they cause 400/500 errors)
phase_content_clean = re.sub(r'```[\s\S]*?```', '[Code block omitted - see roadmap file]', phase_content)
# Then limit description to first 400 chars
description_preview = phase_content_clean[:400]
if len(phase_content_clean) > 400:
description_preview += '...'
# Determine status from title
phase_status = 'FUTURE'
title_upper = phase_title_raw.upper()
# Check for status indicators in the raw title
if ('CURRENT' in title_upper or '' in phase_title_raw or 'DEPLOYED' in title_upper) and 'FUTURE' not in title_upper:
phase_status = 'IN PROGRESS'
elif '🔜' in phase_title_raw or ('NEXT' in title_upper and 'FUTURE' not in title_upper):
phase_status = 'PENDING'
elif 'COMPLETE' in title_upper:
phase_status = 'COMPLETE'
# Otherwise stays FUTURE
# Extract progress if present
progress_match = re.search(r'(\d+)/(\d+)', phase_content[:200])
progress = f"{progress_match.group(1)}/{progress_match.group(2)}" if progress_match else ''
task = {
'title': f"Phase {phase_num}: {phase_title}",
'description': f"""**Initiative:** {filepath.stem.replace('_', ' ').title().replace('Roadmap', '')}
**File:** [{filename}](./{filename})
{description_preview}
""",
'status': phase_status,
'progress': progress,
'due_date': None,
'labels': [filename.replace('.md', '').lower(), f'phase-{phase_num}'],
}
tasks.append(task)
return tasks
def sync_roadmap_to_deck(dry_run: bool = False):
"""Sync roadmap files to Nextcloud Deck"""
print("🔄 Syncing roadmap to Nextcloud Deck...")
print()
api = DeckAPI()
parser = RoadmapParser()
# Note: Nextcloud Deck API doesn't support GET cards by stack in this version
# We'll create cards and rely on manual deduplication for now
print("<EFBFBD> Creating cards from roadmap (manual deduplication required)")
print()
# Parse all roadmap files
all_tasks = []
for filename in ROADMAP_FILES:
filepath = ROADMAP_DIR / filename
if filepath.exists():
print(f"📖 Parsing {filename}...")
tasks = parser.parse_file(filepath)
all_tasks.extend(tasks)
print(f" Found {len(tasks)} tasks")
print()
print(f"📝 Total tasks from roadmaps: {len(all_tasks)}")
print()
# Create/update cards
created = 0
skipped = 0
for task in all_tasks:
title = task['title']
# Create new card
stack_name = STATUS_TO_STACK.get(task['status'], '📥 Backlog')
# Debug output
print(f"✨ Create: {title}")
print(f" Status from roadmap: {task['status']!r}")
print(f" Mapped to stack: {stack_name!r}")
if stack_name not in STACKS:
print(f" ❌ ERROR: Stack {stack_name!r} not found in STACKS")
print(f" Available stacks: {list(STACKS.keys())}")
skipped += 1
continue
stack_id = STACKS[stack_name]
print(f" Stack ID: {stack_id}")
if task['progress']:
print(f" Progress: {task['progress']}")
if not dry_run:
try:
result = api.create_card(
stack_id=stack_id,
title=title,
description=task['description'],
due_date=task['due_date'],
)
print(f" ✅ Created card ID: {result['id']}")
created += 1
except Exception as e:
print(f" ❌ Error: {e}")
import traceback
traceback.print_exc()
skipped += 1
else:
created += 1
print()
print("=" * 60)
print(f"✅ Sync complete!")
print(f" Created: {created}")
print(f" Skipped: {skipped}")
if dry_run:
print()
print(" (Dry run - no changes made)")
def main():
parser = argparse.ArgumentParser(description='Sync roadmap with Nextcloud Deck')
parser.add_argument('--init', action='store_true', help='Initialize: create cards from roadmap')
parser.add_argument('--sync', action='store_true', help='Sync: update existing cards')
parser.add_argument('--dry-run', action='store_true', help='Show what would be done without making changes')
args = parser.parse_args()
if not args.init and not args.sync:
parser.print_help()
sys.exit(1)
try:
sync_roadmap_to_deck(dry_run=args.dry_run)
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == '__main__':
main()