Initial commit — briefing CLI tool
Terminal dashboard for Nodie daily/weekly work summaries with: - Color-coded task display with creation dates and "days ago" format - NEW badge highlighting for tasks created today - Grouped sections: New Today, Needs Work, Waiting, Done - Interactive mode: type a task number to see full details - Calendar rendering with NOW/DONE/NEXT markers - Git commit summary per repo Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
531
briefing
Executable file
531
briefing
Executable file
@@ -0,0 +1,531 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Nodie Briefing — terminal display with interactive mode
|
||||||
|
Usage: briefing [7|30] [client]
|
||||||
|
briefing — 7-day daily briefing
|
||||||
|
briefing 30 — 30-day weekly review
|
||||||
|
briefing 7 OHS — 7-day, OHS only
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json, sys, subprocess, re, readline
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
|
API_KEY = "b666180ef00fffd23b9a606a4bd8deddea274d2e69fab975583d3d966fbfafed"
|
||||||
|
BASE_URL = "https://teacup.nodie.co.za/api/v1"
|
||||||
|
|
||||||
|
# ANSI
|
||||||
|
BOLD = "\033[1m"
|
||||||
|
DIM = "\033[2m"
|
||||||
|
ITALIC = "\033[3m"
|
||||||
|
UNDER = "\033[4m"
|
||||||
|
RESET = "\033[0m"
|
||||||
|
WHITE = "\033[97m"
|
||||||
|
GREY = "\033[90m"
|
||||||
|
CYAN = "\033[36m"
|
||||||
|
GREEN = "\033[32m"
|
||||||
|
YELLOW = "\033[33m"
|
||||||
|
RED = "\033[31m"
|
||||||
|
MAGENTA = "\033[35m"
|
||||||
|
BG_YELLOW = "\033[43m"
|
||||||
|
BG_GREEN = "\033[42m"
|
||||||
|
BLACK = "\033[30m"
|
||||||
|
|
||||||
|
SAST = timezone(timedelta(hours=2))
|
||||||
|
|
||||||
|
# Priority colors
|
||||||
|
PRI_COLOR = {
|
||||||
|
'critical': RED,
|
||||||
|
'high': RED,
|
||||||
|
'medium': YELLOW,
|
||||||
|
'low': GREY,
|
||||||
|
None: GREY,
|
||||||
|
'': GREY,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Status colors
|
||||||
|
STATUS_COLOR = {
|
||||||
|
'in_progress': GREEN,
|
||||||
|
'todo': CYAN,
|
||||||
|
'hold_to_discuss': MAGENTA,
|
||||||
|
'waitingfortesting': YELLOW,
|
||||||
|
'willdemo': YELLOW,
|
||||||
|
'done': DIM,
|
||||||
|
'completed': DIM,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def days_ago(created_at_str):
|
||||||
|
"""Return human-friendly age string."""
|
||||||
|
if not created_at_str:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
created = datetime.strptime(created_at_str, "%Y-%m-%d %H:%M:%S").replace(tzinfo=SAST)
|
||||||
|
now = datetime.now(SAST)
|
||||||
|
delta = now - created
|
||||||
|
if delta.days == 0:
|
||||||
|
return "today"
|
||||||
|
elif delta.days == 1:
|
||||||
|
return "1d ago"
|
||||||
|
elif delta.days < 7:
|
||||||
|
return f"{delta.days}d ago"
|
||||||
|
elif delta.days < 30:
|
||||||
|
weeks = delta.days // 7
|
||||||
|
return f"{weeks}w ago"
|
||||||
|
else:
|
||||||
|
months = delta.days // 30
|
||||||
|
return f"{months}mo ago"
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def is_new(created_at_str):
|
||||||
|
"""True if created today."""
|
||||||
|
if not created_at_str:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
created = datetime.strptime(created_at_str, "%Y-%m-%d %H:%M:%S").replace(tzinfo=SAST)
|
||||||
|
now = datetime.now(SAST)
|
||||||
|
return created.date() == now.date()
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def format_priority(pri):
|
||||||
|
color = PRI_COLOR.get(pri, GREY)
|
||||||
|
return f"{color}{pri or 'unset'}{RESET}"
|
||||||
|
|
||||||
|
|
||||||
|
def format_status(status):
|
||||||
|
color = STATUS_COLOR.get(status, GREY)
|
||||||
|
return f"{color}{status or 'unset'}{RESET}"
|
||||||
|
|
||||||
|
|
||||||
|
def new_badge():
|
||||||
|
return f" {BG_YELLOW}{BLACK} NEW {RESET}"
|
||||||
|
|
||||||
|
|
||||||
|
def print_task(task, indent=" "):
|
||||||
|
"""Print a single task line with metadata."""
|
||||||
|
nid = task['nodesid']
|
||||||
|
name = task['node_name']
|
||||||
|
status = task.get('status')
|
||||||
|
priority = task.get('priority')
|
||||||
|
created = task.get('created_at', '')
|
||||||
|
server = task.get('server')
|
||||||
|
assigned = task.get('on_behalf_of')
|
||||||
|
age = days_ago(created)
|
||||||
|
is_today = is_new(created)
|
||||||
|
|
||||||
|
# Task number + name
|
||||||
|
badge = new_badge() if is_today else ""
|
||||||
|
line = f"{indent}{YELLOW}#{nid}{RESET} {BOLD}{WHITE}{name}{RESET}{badge}"
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
# Meta line
|
||||||
|
parts = [format_status(status), format_priority(priority)]
|
||||||
|
if age:
|
||||||
|
age_color = GREEN if is_today else GREY
|
||||||
|
parts.append(f"{age_color}{age}{RESET}")
|
||||||
|
if server:
|
||||||
|
parts.append(f"{CYAN}{server}{RESET}")
|
||||||
|
if assigned:
|
||||||
|
# Shorten email format
|
||||||
|
short = assigned.split(' (')[0] if ' (' in assigned else assigned
|
||||||
|
parts.append(f"{DIM}For: {short}{RESET}")
|
||||||
|
|
||||||
|
print(f"{indent} {' | '.join(parts)}")
|
||||||
|
|
||||||
|
# Children summary
|
||||||
|
cs = task.get('children_summary')
|
||||||
|
if cs and cs.get('total', 0) > 0:
|
||||||
|
child_parts = []
|
||||||
|
for key in ['in_progress', 'todo', 'waitingfortesting', 'done', 'completed']:
|
||||||
|
if cs.get(key, 0) > 0:
|
||||||
|
child_parts.append(f"{cs[key]} {key}")
|
||||||
|
if child_parts:
|
||||||
|
print(f"{indent} {GREY}Subtasks: {', '.join(child_parts)}{RESET}")
|
||||||
|
|
||||||
|
# Latest note preview
|
||||||
|
note = task.get('latest_note')
|
||||||
|
if note:
|
||||||
|
text = note.get('content', '')
|
||||||
|
if len(text) > 100:
|
||||||
|
text = text[:100] + "..."
|
||||||
|
print(f"{indent} {DIM}{ITALIC}{text}{RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def render_calendar(cal_data):
|
||||||
|
"""Render calendar section from raw data."""
|
||||||
|
if not cal_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n{BOLD}{CYAN}{'─' * 60}{RESET}")
|
||||||
|
print(f"{BOLD}{CYAN}Calendar{RESET}")
|
||||||
|
print(f"{BOLD}{CYAN}{'─' * 60}{RESET}")
|
||||||
|
|
||||||
|
now = datetime.now(SAST)
|
||||||
|
|
||||||
|
for date_str, events in cal_data.items():
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(date_str, "%Y-%m-%d")
|
||||||
|
if dt.date() == now.date():
|
||||||
|
label = f"{dt.strftime('%a %d %b')} (today)"
|
||||||
|
elif dt.date() == (now + timedelta(days=1)).date():
|
||||||
|
label = f"{dt.strftime('%a %d %b')} (tomorrow)"
|
||||||
|
else:
|
||||||
|
label = dt.strftime('%a %d %b')
|
||||||
|
except ValueError:
|
||||||
|
label = date_str
|
||||||
|
|
||||||
|
print(f"\n {BOLD}{WHITE}{label}{RESET}")
|
||||||
|
|
||||||
|
for ev in events:
|
||||||
|
start = ev.get('start_datetime', '')
|
||||||
|
end = ev.get('end_datetime', '')
|
||||||
|
title = ev.get('title', '')
|
||||||
|
|
||||||
|
# Format times
|
||||||
|
try:
|
||||||
|
s = datetime.strptime(start, "%Y-%m-%d %H:%M:%S")
|
||||||
|
e = datetime.strptime(end, "%Y-%m-%d %H:%M:%S")
|
||||||
|
time_str = f"{s.strftime('%H:%M')}–{e.strftime('%H:%M')}"
|
||||||
|
|
||||||
|
# Status marker
|
||||||
|
if s.replace(tzinfo=SAST) < now < e.replace(tzinfo=SAST):
|
||||||
|
marker = f" {BG_GREEN}{BLACK} NOW {RESET}"
|
||||||
|
elif e.replace(tzinfo=SAST) < now:
|
||||||
|
marker = f" {GREY}[DONE]{RESET}"
|
||||||
|
elif s.replace(tzinfo=SAST) > now and (s.replace(tzinfo=SAST) - now).seconds < 3600:
|
||||||
|
marker = f" {CYAN}[NEXT]{RESET}"
|
||||||
|
else:
|
||||||
|
marker = ""
|
||||||
|
except ValueError:
|
||||||
|
time_str = start
|
||||||
|
marker = ""
|
||||||
|
|
||||||
|
print(f" {DIM}{time_str}{RESET} {title}{marker}")
|
||||||
|
|
||||||
|
|
||||||
|
def render_tasks(tasks):
|
||||||
|
"""Render tasks grouped by category, with NEW section first."""
|
||||||
|
if not tasks:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Separate new tasks
|
||||||
|
new_tasks = [t for t in tasks if t.get('category') == 'actionable' and is_new(t.get('created_at', ''))]
|
||||||
|
actionable = [t for t in tasks if t.get('category') == 'actionable' and not is_new(t.get('created_at', ''))]
|
||||||
|
waiting = [t for t in tasks if t.get('category') == 'waiting']
|
||||||
|
done = [t for t in tasks if t.get('category') == 'done']
|
||||||
|
|
||||||
|
# New today
|
||||||
|
if new_tasks:
|
||||||
|
print(f"\n{BOLD}{RED}{'─' * 60}{RESET}")
|
||||||
|
print(f"{BOLD}{RED}New Today ({len(new_tasks)}){RESET}")
|
||||||
|
print(f"{BOLD}{RED}{'─' * 60}{RESET}")
|
||||||
|
by_client = group_by_client(new_tasks)
|
||||||
|
for client, projects in by_client.items():
|
||||||
|
print(f"\n {BOLD}{WHITE}{client}{RESET}")
|
||||||
|
for project, ptasks in projects.items():
|
||||||
|
if project:
|
||||||
|
print(f" {BOLD}{GREY}{project}{RESET}")
|
||||||
|
for t in ptasks:
|
||||||
|
print_task(t, " ")
|
||||||
|
|
||||||
|
# Actionable
|
||||||
|
if actionable:
|
||||||
|
print(f"\n{BOLD}{CYAN}{'─' * 60}{RESET}")
|
||||||
|
print(f"{BOLD}{CYAN}Needs Work ({len(actionable)}){RESET}")
|
||||||
|
print(f"{BOLD}{CYAN}{'─' * 60}{RESET}")
|
||||||
|
by_client = group_by_client(actionable)
|
||||||
|
for client, projects in by_client.items():
|
||||||
|
print(f"\n {BOLD}{WHITE}{client}{RESET}")
|
||||||
|
for project, ptasks in projects.items():
|
||||||
|
if project:
|
||||||
|
print(f" {BOLD}{GREY}{project}{RESET}")
|
||||||
|
for t in ptasks:
|
||||||
|
print_task(t, " ")
|
||||||
|
|
||||||
|
# Waiting
|
||||||
|
if waiting:
|
||||||
|
print(f"\n{BOLD}{YELLOW}{'─' * 60}{RESET}")
|
||||||
|
print(f"{BOLD}{YELLOW}Waiting on Others ({len(waiting)}){RESET}")
|
||||||
|
print(f"{BOLD}{YELLOW}{'─' * 60}{RESET}")
|
||||||
|
by_client = group_by_client(waiting)
|
||||||
|
for client, projects in by_client.items():
|
||||||
|
print(f"\n {BOLD}{WHITE}{client}{RESET}")
|
||||||
|
for project, ptasks in projects.items():
|
||||||
|
if project:
|
||||||
|
print(f" {BOLD}{GREY}{project}{RESET}")
|
||||||
|
for t in ptasks:
|
||||||
|
print_task(t, " ")
|
||||||
|
|
||||||
|
# Done
|
||||||
|
if done:
|
||||||
|
print(f"\n{BOLD}{GREEN}{'─' * 60}{RESET}")
|
||||||
|
print(f"{BOLD}{GREEN}Recently Completed ({len(done)}){RESET}")
|
||||||
|
print(f"{BOLD}{GREEN}{'─' * 60}{RESET}")
|
||||||
|
by_client = group_by_client(done)
|
||||||
|
for client, projects in by_client.items():
|
||||||
|
print(f"\n {BOLD}{WHITE}{client}{RESET}")
|
||||||
|
for project, ptasks in projects.items():
|
||||||
|
if project:
|
||||||
|
print(f" {BOLD}{GREY}{project}{RESET}")
|
||||||
|
for t in ptasks:
|
||||||
|
print_task(t, " ")
|
||||||
|
|
||||||
|
|
||||||
|
def group_by_client(tasks):
|
||||||
|
"""Group tasks by client > project, preserving order."""
|
||||||
|
result = {}
|
||||||
|
for t in tasks:
|
||||||
|
client = t.get('client') or 'Unknown'
|
||||||
|
project = t.get('project') or ''
|
||||||
|
if client not in result:
|
||||||
|
result[client] = {}
|
||||||
|
if project not in result[client]:
|
||||||
|
result[client][project] = []
|
||||||
|
result[client][project].append(t)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def render_commits(commits):
|
||||||
|
"""Render git commit summary."""
|
||||||
|
if not commits:
|
||||||
|
return
|
||||||
|
|
||||||
|
total = sum(len(c) for c in commits.values())
|
||||||
|
print(f"\n{BOLD}{CYAN}{'─' * 60}{RESET}")
|
||||||
|
print(f"{BOLD}{CYAN}Commits ({total}){RESET}")
|
||||||
|
print(f"{BOLD}{CYAN}{'─' * 60}{RESET}")
|
||||||
|
|
||||||
|
for repo, repo_commits in commits.items():
|
||||||
|
print(f"\n {BOLD}{WHITE}{repo}{RESET} {GREY}({len(repo_commits)}){RESET}")
|
||||||
|
for c in repo_commits[:5]: # Show max 5 per repo
|
||||||
|
sha = c.get('sha', '')[:7]
|
||||||
|
date = c.get('date', '')[:10]
|
||||||
|
msg = c.get('message', '').split('\n')[0]
|
||||||
|
if len(msg) > 70:
|
||||||
|
msg = msg[:70] + "..."
|
||||||
|
print(f" {GREY}{sha} {date}{RESET} {msg}")
|
||||||
|
if len(repo_commits) > 5:
|
||||||
|
print(f" {DIM}... +{len(repo_commits) - 5} more{RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def render_summary(summary, range_days):
|
||||||
|
"""Render the summary header."""
|
||||||
|
now = datetime.now(SAST)
|
||||||
|
print(f"\n{BOLD}{WHITE}{now.strftime('%a %d %b, %H:%M')}{RESET}")
|
||||||
|
|
||||||
|
a = summary.get('actionable', 0)
|
||||||
|
w = summary.get('waiting', 0)
|
||||||
|
b = summary.get('blocked', 0)
|
||||||
|
d = summary.get('done', 0)
|
||||||
|
m = summary.get('meetings_today', 0)
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
if a: parts.append(f"{CYAN}{a} actionable{RESET}")
|
||||||
|
if w: parts.append(f"{YELLOW}{w} waiting{RESET}")
|
||||||
|
if b: parts.append(f"{RED}{b} blocked{RESET}")
|
||||||
|
if d: parts.append(f"{GREEN}{d} done{RESET}")
|
||||||
|
print(f" {' | '.join(parts)} — {range_days}-day window")
|
||||||
|
if m:
|
||||||
|
print(f" {MAGENTA}{m} meeting{'s' if m != 1 else ''} today{RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_task_detail(nid):
|
||||||
|
"""Fetch full task detail for interactive mode."""
|
||||||
|
result = subprocess.run(
|
||||||
|
["curl", "-s", "-H", f"Authorization: Bearer {API_KEY}",
|
||||||
|
f"{BASE_URL}/nodes/{nid}"],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
data = json.loads(result.stdout)
|
||||||
|
if data.get('success'):
|
||||||
|
return data['data']
|
||||||
|
except (json.JSONDecodeError, KeyError):
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def show_task_detail(nid):
|
||||||
|
"""Show full task detail in interactive mode."""
|
||||||
|
print(f"\n {DIM}Fetching #{nid}...{RESET}")
|
||||||
|
data = fetch_task_detail(nid)
|
||||||
|
if not data:
|
||||||
|
print(f" {RED}Could not fetch task #{nid}{RESET}")
|
||||||
|
return
|
||||||
|
|
||||||
|
node = data.get('node', {})
|
||||||
|
details = data.get('details', {})
|
||||||
|
bc = data.get('breadcrumb', {})
|
||||||
|
children = data.get('children', [])
|
||||||
|
|
||||||
|
# Header
|
||||||
|
print(f"\n {BOLD}{WHITE}#{node.get('nodesid')} — {node.get('node_name')}{RESET}")
|
||||||
|
print(f" {GREY}{'─' * 50}{RESET}")
|
||||||
|
|
||||||
|
# Breadcrumb
|
||||||
|
bc_parts = []
|
||||||
|
if bc.get('client'):
|
||||||
|
bc_parts.append(bc['client'].get('node_name', ''))
|
||||||
|
if bc.get('project'):
|
||||||
|
bc_parts.append(bc['project'].get('node_name', ''))
|
||||||
|
if bc.get('category'):
|
||||||
|
bc_parts.append(bc['category'].get('node_name', ''))
|
||||||
|
if bc_parts:
|
||||||
|
print(f" {DIM}{' > '.join(bc_parts)}{RESET}")
|
||||||
|
|
||||||
|
# Dates
|
||||||
|
created = node.get('created_at', '')
|
||||||
|
updated = node.get('updated_at', '')
|
||||||
|
created_by = node.get('created_by', '')
|
||||||
|
age = days_ago(created)
|
||||||
|
print(f" {DIM}Created: {created} ({age}){' by ' + created_by if created_by else ''}{RESET}")
|
||||||
|
if updated != created:
|
||||||
|
print(f" {DIM}Updated: {updated} ({days_ago(updated)}){RESET}")
|
||||||
|
|
||||||
|
# Details
|
||||||
|
if isinstance(details, dict):
|
||||||
|
status = details.get('status') or details.get('taskStatus') or details.get('subtaskStatus')
|
||||||
|
priority = details.get('priority') or details.get('taskPriority') or details.get('subtaskPriority')
|
||||||
|
server = details.get('server')
|
||||||
|
assigned = details.get('on_behalf_of')
|
||||||
|
completion = details.get('completion') or details.get('taskCompletion') or details.get('subtaskCompletion')
|
||||||
|
|
||||||
|
meta = []
|
||||||
|
if status: meta.append(f"Status: {format_status(status)}")
|
||||||
|
if priority: meta.append(f"Priority: {format_priority(priority)}")
|
||||||
|
if completion and completion != '0': meta.append(f"Completion: {completion}%")
|
||||||
|
if server: meta.append(f"Server: {CYAN}{server}{RESET}")
|
||||||
|
if assigned: meta.append(f"For: {assigned}")
|
||||||
|
for m in meta:
|
||||||
|
print(f" {m}")
|
||||||
|
|
||||||
|
# Description
|
||||||
|
desc = details.get('description')
|
||||||
|
if desc:
|
||||||
|
print(f"\n {WHITE}{desc}{RESET}")
|
||||||
|
|
||||||
|
# Note content (from ticket)
|
||||||
|
note = details.get('note_content')
|
||||||
|
if note:
|
||||||
|
# Strip HTML
|
||||||
|
clean = re.sub(r'<[^>]+>', ' ', note)
|
||||||
|
clean = re.sub(r'\s+', ' ', clean).strip()
|
||||||
|
if len(clean) > 500:
|
||||||
|
clean = clean[:500] + "..."
|
||||||
|
print(f"\n {DIM}{clean}{RESET}")
|
||||||
|
|
||||||
|
# Children
|
||||||
|
if children:
|
||||||
|
subtasks = [c for c in children if c.get('node_typesid') in (4, 5)]
|
||||||
|
notes = [c for c in children if c.get('node_typesid') == 6]
|
||||||
|
|
||||||
|
if subtasks:
|
||||||
|
print(f"\n {BOLD}Subtasks ({len(subtasks)}):{RESET}")
|
||||||
|
for st in subtasks:
|
||||||
|
st_details = st.get('details', {})
|
||||||
|
st_status = ''
|
||||||
|
if isinstance(st_details, dict):
|
||||||
|
st_status = st_details.get('status') or st_details.get('subtaskStatus') or ''
|
||||||
|
print(f" {YELLOW}#{st['nodesid']}{RESET} {st['node_name']} {GREY}[{st_status}]{RESET}")
|
||||||
|
|
||||||
|
if notes:
|
||||||
|
print(f"\n {BOLD}Notes ({len(notes)}):{RESET}")
|
||||||
|
for n in notes[-3:]: # Show last 3 notes
|
||||||
|
n_details = n.get('details', {})
|
||||||
|
content = ''
|
||||||
|
if isinstance(n_details, dict):
|
||||||
|
content = n_details.get('note_content') or n_details.get('description') or ''
|
||||||
|
content = re.sub(r'<[^>]+>', ' ', content)
|
||||||
|
content = re.sub(r'\s+', ' ', content).strip()
|
||||||
|
if len(content) > 200:
|
||||||
|
content = content[:200] + "..."
|
||||||
|
n_age = days_ago(n.get('created_at', ''))
|
||||||
|
print(f" {GREY}{n.get('created_at', '')[:10]} ({n_age}){RESET}")
|
||||||
|
print(f" {DIM}{content}{RESET}")
|
||||||
|
|
||||||
|
print(f"\n {DIM}URL: https://teacup.nodie.co.za/?sn={node.get('nodesid')}{RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def interactive_mode(tasks):
|
||||||
|
"""Interactive prompt — type a task number to see details."""
|
||||||
|
# Build lookup
|
||||||
|
task_ids = set()
|
||||||
|
for t in tasks:
|
||||||
|
task_ids.add(str(t['nodesid']))
|
||||||
|
|
||||||
|
print(f"\n{BOLD}{CYAN}{'─' * 60}{RESET}")
|
||||||
|
print(f"{DIM}Type a task number for details, or 'q' to quit{RESET}")
|
||||||
|
print(f"{BOLD}{CYAN}{'─' * 60}{RESET}")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
inp = input(f"\n{BOLD}{CYAN}#{RESET} ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print()
|
||||||
|
break
|
||||||
|
|
||||||
|
if not inp or inp.lower() in ('q', 'quit', 'exit'):
|
||||||
|
break
|
||||||
|
|
||||||
|
# Strip # prefix if present
|
||||||
|
inp = inp.lstrip('#')
|
||||||
|
|
||||||
|
if inp.isdigit():
|
||||||
|
show_task_detail(int(inp))
|
||||||
|
else:
|
||||||
|
print(f" {DIM}Enter a task number (e.g. 3047) or 'q' to quit{RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
range_days = "7"
|
||||||
|
client = None
|
||||||
|
|
||||||
|
args = sys.argv[1:]
|
||||||
|
if args and args[0].isdigit():
|
||||||
|
range_days = args.pop(0)
|
||||||
|
if args:
|
||||||
|
client = args[0]
|
||||||
|
|
||||||
|
url = f"{BASE_URL}/briefing?range={range_days}"
|
||||||
|
if client:
|
||||||
|
url += f"&client={client}"
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["curl", "-s", "-H", f"Authorization: Bearer {API_KEY}", url],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result.stdout.strip():
|
||||||
|
print(f"{RED}Empty response from API{RESET}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(result.stdout)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"{RED}Invalid JSON response{RESET}")
|
||||||
|
print(result.stdout[:500])
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not data.get("success"):
|
||||||
|
print(f"{RED}API error: {data.get('message', 'Unknown')}{RESET}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
raw = data["data"]
|
||||||
|
|
||||||
|
# Render from raw JSON data
|
||||||
|
render_summary(raw.get('summary', {}), int(range_days))
|
||||||
|
render_calendar(raw.get('calendar', {}))
|
||||||
|
render_tasks(raw.get('tasks', []))
|
||||||
|
render_commits(raw.get('git_commits', {}))
|
||||||
|
|
||||||
|
# Interactive mode
|
||||||
|
tasks = raw.get('tasks', [])
|
||||||
|
if tasks and sys.stdin.isatty():
|
||||||
|
interactive_mode(tasks)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user