Blueworks Live

Blueworks Live

Connect with IBM subject matter experts and get answers to your biggest concerns. Also, share ideas, benchmarks, best practices, and lessons learned with other users. As a member of this online user community

 View Only

Driving Adoption with Activity Insights in IBM Blueworks Live: A Practical Guide to Usage Dashboards

By Sunith M S posted 05/13/26 07:59 AM

  

Organisations invest heavily in documenting business processes, yet many lack visibility into whether those processes are actively used. This disconnect creates a critical blind spot, making it difficult to measure adoption, evaluate effectiveness, and maximise the value of process management initiatives.

IBM Blueworks Live captures detailed activity data—including logins, views, changes, and snapshots—but this information is often underutilised because it exists as individual event records. By transforming this data into visual dashboards, organisations can uncover meaningful insights into user adoption, content engagement, and governance practices—enabling more informed decisions and stronger process outcomes.

Leveraging Activity API Data for Smarter Decision-Making

Measuring process adoption and engagement starts with access to reliable activity data. IBM Blueworks Live provides this through the Activity API, a comprehensive interface that captures key user interactions across the platform.

This API provides visibility into key platform interactions, including user logins, content views, process changes, and snapshot creation. It supports filtering by time period, activity type, and user access permissions—providing a reliable foundation for activity analysis.

Raw API data consists of individual event records—each representing a single user action at a specific moment. While comprehensive, this granular data requires aggregation and structuring to reveal meaningful patterns. The key to unlocking actionable insights lies in transforming these discrete events into visual dashboards that highlight trends, identify anomalies, and support data-driven decision-making.

Raw Activity Data to Actionable Insights

Transforming detailed activity records into strategic intelligence requires organising data into purpose-built dashboards, each aligned to a critical dimension of user engagement. This structured approach provides comprehensive visibility into how users access, consume, maintain, and process content over time.

The dashboard framework addresses three key engagement areas:

  • Access patterns reveal platform adoption through login activity, showing who engages with the system and how frequently.
  • Content consumption tracks which processes users reference, identifying high-value content and underutilised resources.
  • Maintenance activity highlights active contributors who update and improve process documentation, ensuring content remains current.

User Logins Dashboard

Login activity serves as the primary indicator of platform adoption. As the gateway to all other interactions, login patterns reveal who actively engages with the platform, how frequently they access it, and whether adoption is growing or declining over time.
This dashboard enables organisations to: 

  • Identify engagement gaps by spotting inactive users who may need additional support
  • Recognise active users who consistently engage with the platform
  • Measure training effectiveness by correlating login increases with onboarding initiative

The insights support targeted interventions—whether re-engaging dormant users, adjusting training approaches based on adoption patterns, or recognising teams that demonstrate strong platform utilisation. By monitoring login trends, organisations can address adoption challenges proactively rather than discovering them months later through anecdotal feedback.

User Logins Dashboard

   User Logins Dashboard - Track daily access patterns and adoption trends


Items Viewed Dashboard

Viewing activity reveals how users interact with content across the platform. This dashboard tracks the volume and patterns of user engagement—how frequently each user views content, when activity peaks occur, and how engagement varies across the team.

The dashboard displays daily interaction counts for each user, creating a clear picture of:

  • Individual engagement levels showing which users actively reference content versus those with minimal interaction
  • Temporal patterns revealing when teams most frequently access process documentation
  • Engagement consistency distinguishing between sporadic and regular content consumers
  • Team-wide trends highlighting periods of high or low platform utilisation

This user-centric view enables organisations to understand engagement behaviour at both individual and aggregate levels. Teams can identify highly engaged users who may serve as adoption leaders, spot users who may need additional training or support, and recognise patterns that correlate with business cycles or operational needs.
The data shifts focus from content inventory to actual user behaviour—a critical distinction for driving adoption and demonstrating value.

Items Viewed Dashboard

  Items Viewed Dashboard - Monitor content consumption and engagement patterns


Items Changed Dashboard

Change activity highlights active contribution to process maintenance and continuous improvement. This dashboard tracks who modifies content, how frequently updates occur, and how modification activity is distributed across users and time periods.

The dashboard displays daily change counts for each user, revealing:

  • Active contributors who consistently maintain and improve process documentation
  • Modification patterns showing when updates typically occur and whether they align with business cycles
  • Contribution distribution identifying whether maintenance responsibility is shared or concentrated
  • Activity gaps highlighting periods or users with minimal modification activity

These insights help organisations recognise process champions who drive continuous improvement, identify content that may be stagnant and require review, and ensure maintenance responsibilities are appropriately distributed. By tracking who makes changes and when, teams can foster collaboration, acknowledge valuable contributions, and maintain documentation that accurately reflects current operational practices. The data transforms process maintenance from an invisible activity into a measurable, manageable practice.

Items Changed Dashboard

   Items Changed Dashboard - Analyse process modification and maintenance activity


Snapshots Dashboard

Snapshot activity reflects how processes evolve through versioning and governance practices. This dashboard tracks who creates snapshots, when versioning occurs, and how snapshot creation patterns align with organisational change management procedures.

The dashboard displays daily snapshot counts for each user, revealing:

  • Governance adherence showing whether users follow version control practices consistently
  • Version control patterns indicating when teams typically create snapshots and whether timing aligns with change cycles
  • Responsibility distribution identifying who manages process versioning across the organisation
  • Compliance indicators highlighting whether critical processes receive appropriate version management

By tracking snapshot creation activity, organisations can ensure content remains properly governed and aligned with operational or regulatory standards. The data provides concrete evidence of governance practices, identifies gaps in version control discipline, and enables teams to demonstrate compliance through measurable activity. For organisations with strict change management requirements, this visibility transforms governance from a policy document into a verifiable, data-driven practice.

Snapshots Dashboard

  Snapshots Dashboard - Track version control and governance practices


Combined Overview Dashboard

The Combined Overview Dashboard consolidates all major activity types into a unified perspective, aggregating logins, views, changes, and snapshots into a single comparative view. This holistic approach reveals how different engagement dimensions relate to each other over time.

This executive-level view supports strategic decision-making by revealing patterns that individual dashboards cannot show. For example, consistent logins with declining views might indicate navigation issues, while high views with low changes could suggest content is meeting needs without requiring updates. The combined perspective enables leaders to assess overall platform effectiveness, communicate value to stakeholders, and make informed decisions about where to focus improvement efforts.

Combined Overview Dashboard

  Combined Overview Dashboard - Comprehensive view of all activity types


Key Takeaways

Transforming activity data into structured dashboards provides organisations with clear visibility into how users access, engage with, and maintain process content over time. This visibility shifts process management from assumption-based to evidence-based decision-making. 

The dashboard approach delivers measurable benefits:

Monitor adoption patterns and address engagement gaps before they impact operations
Strengthen governance practices with concrete evidence of version control and compliance 
Identify optimisation opportunities based on actual usage patterns rather than intuition 
Drive continuous improvement through data that reveals what works and what needs attention 
Demonstrate business value with quantifiable metrics that justify process management investments

Implementation Script

The Python script below provides a complete solution for extracting activity data from the Blueworks Live API and generating excel dashboards

Requirements: Python 3.6 or higher. To successfully connect to the Blueworks Live API, mandatory parameters such as the Base URL, API access token, start date, and end date must be updated in the script below before execution.

View Full Python Script

#!/usr/bin/env python3
"""
Blueworks Live All Activities Chart Generator

This script fetches multiple activity types from Blueworks Live API and creates
an Excel file with separate worksheets for each activity type plus a combined chart.
"""

import requests
import json
from datetime import datetime, timedelta
from collections import defaultdict
import openpyxl
from openpyxl.chart import LineChart, Reference
from openpyxl.chart.marker import Marker
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from openpyxl.utils import get_column_letter
from openpyxl.drawing.line import LineProperties
import sys

# ============================================================================
# CONFIGURATION - Modify these values according to your needs
# ============================================================================

# API Configuration
API_BASE_URL = ""  # Your Blueworks Live server URL (Required)
ACCESS_TOKEN = ""  # Your Bearer access token (Required)
VERIFY_SSL = True  

# Date Range Configuration
# Format: "YYYY-MM-DDTHH:MM:SS.sss±HH:MM" (ISO 8601)
# WARNING: The Blueworks Live API has a maximum 31-day range limit
START_DATE = ""  # Start date for data fetch (Required)
END_DATE = ""    # End date for data fetch (max 31 days from start) (Required)

# Output Configuration
OUTPUT_FILENAME = "blueworks_all_activities_chart.xlsx"  # Name of the generated Excel file

# Activity Types to Fetch
# You can comment out any activity type you don't want to include
ACTIVITY_TYPES = {
    "LOGINS": "User Logins",
    "ITEMS_VIEWED": "Items Viewed",
    "ITEMS_CHANGED": "Items Changed",
    "SNAPSHOTS": "Snapshots"
}

# Chart Configuration
CHART_HEIGHT = 16  # Chart height in units
CHART_WIDTH = 28   # Chart width in units
MARKER_SIZE = 7    # Size of circular markers on data points
LINE_WIDTH = 25000 # Line width in EMUs (25000 = 2.5pt)

# Color Configuration for Users (in individual activity charts)
# Note: If you have more users than colors, the script will automatically cycle through them
# Add more colors here if you have many users to avoid color repetition
USER_COLORS = [
    "4472C4",  # Blue
    "ED7D31",  # Orange
    "A5A5A5",  # Gray
    "FFC000",  # Yellow
    "5B9BD5",  # Light Blue
    "70AD47",  # Green
    "264478",  # Dark Blue
    "9E480E",  # Brown
    "636363",  # Dark Gray
    "997300",  # Dark Yellow
    "C00000",  # Red
    "7030A0",  # Purple
    "00B0F0",  # Sky Blue
    "92D050",  # Light Green
    "FF6600",  # Bright Orange
    "00B050",  # Emerald
    "0070C0",  # Ocean Blue
    "C65911",  # Rust
    "7F7F7F",  # Medium Gray
    "FFC90E",  # Gold
]

# ============================================================================
# END OF CONFIGURATION
# ============================================================================




def fetch_activities(start_date, end_date, activity_type, access_token, base_url, verify_ssl=True):
    """
    Fetch activities from Blueworks Live API with comprehensive error handling
    
    Args:
        start_date (str): Start date in ISO 8601 format
        end_date (str): End date in ISO 8601 format
        activity_type (str): Type of activity (LOGINS, ITEMS_VIEWED, etc.)
        access_token (str): Bearer token for authentication
        base_url (str): Base URL of the Blueworks Live server
        verify_ssl (bool): Whether to verify SSL certificates
    
    Returns:
        dict: JSON response containing activity records, or empty dict with error info on failure
    """
    # Build URL manually to avoid encoding issues
    url = f"{base_url}/scr/api/activity?startDate={start_date}&endDate={end_date}&type={activity_type}"
    headers = {
        "Authorization": f"Bearer {access_token}"
    }
    
    # Disable SSL warnings if verification is disabled
    if not verify_ssl:
        try:
            import urllib3  # type: ignore
            urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
        except ImportError:
            # urllib3 not available, SSL warnings will still appear
            pass
    
    try:
        print(f"Fetching {activity_type}...")
        response = requests.get(url, headers=headers, verify=verify_ssl, timeout=30)
        print(f"Response status: {response.status_code}")
        
        # Handle different HTTP status codes
        if response.status_code == 200:
            try:
                return response.json()
            except json.JSONDecodeError as e:
                print(f"⚠️  Warning: Invalid JSON response for {activity_type}")
                print(f"   Error: {e}")
                return {"records": [], "error": "Invalid JSON response"}
        
        elif response.status_code == 401:
            print(f"❌ Authentication failed for {activity_type}")
            print(f"   Please check your ACCESS_TOKEN")
            return {"records": [], "error": "Authentication failed"}
        
        elif response.status_code == 403:
            print(f"❌ Access forbidden for {activity_type}")
            print(f"   You may not have permission to access this resource")
            return {"records": [], "error": "Access forbidden"}
        
        elif response.status_code == 400:
            error_msg = response.text[:200]
            print(f"❌ Bad request for {activity_type}")
            if "31 days" in error_msg or "date" in error_msg.lower():
                print(f"   Date range issue: {error_msg}")
                print(f"   The API requires date ranges of 31 days or less")
            else:
                print(f"   Error: {error_msg}")
            return {"records": [], "error": "Bad request - check date range"}
        
        elif response.status_code == 404:
            print(f"⚠️  Warning: Endpoint not found for {activity_type}")
            print(f"   URL: {url}")
            return {"records": [], "error": "Endpoint not found"}
        
        elif response.status_code >= 500:
            print(f"❌ Server error ({response.status_code}) for {activity_type}")
            print(f"   The Blueworks Live server may be experiencing issues")
            return {"records": [], "error": f"Server error {response.status_code}"}
        
        else:
            print(f"⚠️  Unexpected status code {response.status_code} for {activity_type}")
            print(f"   Response: {response.text[:200]}")
            return {"records": [], "error": f"HTTP {response.status_code}"}
    
    except requests.exceptions.Timeout:
        print(f"❌ Request timeout for {activity_type}")
        print(f"   The server took too long to respond (>30 seconds)")
        return {"records": [], "error": "Request timeout"}
    
    except requests.exceptions.SSLError as e:
        print(f"❌ SSL certificate error for {activity_type}")
        print(f"   Try setting VERIFY_SSL = False for self-signed certificates")
        print(f"   Error: {str(e)[:100]}")
        return {"records": [], "error": "SSL certificate error"}
    
    except requests.exceptions.ConnectionError as e:
        print(f"❌ Connection error for {activity_type}")
        print(f"   Could not connect to {base_url}")
        print(f"   Error: {str(e)[:100]}")
        return {"records": [], "error": "Connection failed"}
    
    except requests.exceptions.RequestException as e:
        print(f"❌ Unexpected error fetching {activity_type}")
        print(f"   Error: {str(e)[:100]}")
        if hasattr(e, 'response') and e.response is not None:
            print(f"   Response: {e.response.text[:200]}")
        return {"records": [], "error": f"Request failed: {type(e).__name__}"}
    
    except Exception as e:
        print(f"❌ Unexpected error for {activity_type}")
        print(f"   Error: {str(e)[:100]}")
        return {"records": [], "error": f"Unexpected error: {type(e).__name__}"}

def parse_activity_data(api_response):
    """
    Parse API response and aggregate activity counts by user and date
    
    Args:
        api_response (dict): JSON response from API
    
    Returns:
        dict: Nested dictionary with structure {date: {user: count}}
    """
    activity_counts = defaultdict(lambda: defaultdict(int))
    
    for record in api_response.get("records", []):
        user = record.get("user")
        timestamp = record.get("timeStamp")
        
        if user and timestamp:
            # Extract date from timestamp (YYYY-MM-DD)
            date = timestamp.split("T")[0]
            activity_counts[date][user] += 1
    
    return activity_counts

def create_data_worksheet(wb, sheet_name, activity_counts, all_users, all_dates):
    """Create a data worksheet with formatted data"""
    ws = wb.create_sheet(sheet_name)
    
    header_color = "2C3E50"
    alt_row_color = "ECF0F1"
    
    # Headers
    ws["A1"] = "Date"
    ws["A1"].font = Font(bold=True, color="FFFFFF", size=12)
    ws["A1"].fill = PatternFill(start_color=header_color, end_color=header_color, fill_type="solid")
    ws["A1"].alignment = Alignment(horizontal="center", vertical="center")
    
    for idx, user in enumerate(all_users, start=2):
        cell = ws.cell(row=1, column=idx)
        cell.value = user.title()
        cell.font = Font(bold=True, color="FFFFFF", size=12)
        cell.fill = PatternFill(start_color=header_color, end_color=header_color, fill_type="solid")
        cell.alignment = Alignment(horizontal="center", vertical="center")
    
    # Data rows with formatted dates
    for row_idx, date in enumerate(all_dates, start=2):
        # Format date as "DD MMM"
        try:
            date_obj = datetime.strptime(date, "%Y-%m-%d")
            formatted_date = date_obj.strftime("%d %b")
        except:
            formatted_date = date
        
        date_cell = ws.cell(row=row_idx, column=1, value=formatted_date)
        date_cell.alignment = Alignment(horizontal="center", vertical="center")
        date_cell.font = Font(size=11)
        
        if row_idx % 2 == 0:
            date_cell.fill = PatternFill(start_color=alt_row_color, end_color=alt_row_color, fill_type="solid")
        
        for col_idx, user in enumerate(all_users, start=2):
            count = activity_counts[date].get(user, 0)
            count_cell = ws.cell(row=row_idx, column=col_idx, value=count)
            count_cell.alignment = Alignment(horizontal="center", vertical="center")
            count_cell.font = Font(size=11)
            
            if row_idx % 2 == 0:
                count_cell.fill = PatternFill(start_color=alt_row_color, end_color=alt_row_color, fill_type="solid")
    
    # Auto-adjust column widths
    for col_idx in range(1, len(all_users) + 2):
        column_letter = get_column_letter(col_idx)
        ws.column_dimensions[column_letter].width = 15
    
    # Add borders
    thin_border = Border(
        left=Side(style='thin', color='CCCCCC'),
        right=Side(style='thin', color='CCCCCC'),
        top=Side(style='thin', color='CCCCCC'),
        bottom=Side(style='thin', color='CCCCCC')
    )
    
    for row in ws.iter_rows(min_row=1, max_row=len(all_dates) + 1, 
                            min_col=1, max_col=len(all_users) + 1):
        for cell in row:
            cell.border = thin_border
    
    return ws

def create_chart(ws_data, all_users, all_dates, title, user_colors):
    """Create a spline chart for the data"""
    from openpyxl.chart.shapes import GraphicalProperties
    from openpyxl.drawing.line import LineProperties as DrawingLineProperties
    
    # Individual activity charts with specific dimensions
    # Height: 6.3 inches = 16 cm
    # Width: 17.8 inches = 45.2 cm (rounded to 45)
    chart = LineChart()
    chart.style = 13
    chart.title = title
    chart.height = 16  # 6.3 inches
    chart.width = 45  # 17.8 inches
    chart.smooth = True
    
    # Define data range
    data = Reference(ws_data, min_col=2, min_row=1, max_row=len(all_dates) + 1, max_col=len(all_users) + 1)
    cats = Reference(ws_data, min_col=1, min_row=2, max_row=len(all_dates) + 1)
    
    chart.add_data(data, titles_from_data=True)
    chart.set_categories(cats)
    
    # Configure Y-axis - set minimum to 0 to avoid negative values
    chart.y_axis.title = "Count"
    chart.y_axis.crossAx = 500
    chart.y_axis.axId = 100
    chart.y_axis.delete = False
    chart.y_axis.scaling.min = 0  # Prevent negative values on Y-axis
    
    # Add light horizontal gridlines
    if chart.y_axis.majorGridlines is None:
        from openpyxl.chart.axis import ChartLines
        chart.y_axis.majorGridlines = ChartLines()
    
    gridline_props = GraphicalProperties()
    gridline_line = DrawingLineProperties()
    gridline_line.solidFill = "E0E0E0"
    gridline_line.width = 9525
    gridline_props.line = gridline_line
    chart.y_axis.majorGridlines.spPr = gridline_props
    
    # Configure X-axis
    chart.x_axis.title = ""
    chart.x_axis.crossAx = 100
    chart.x_axis.axId = 500
    chart.x_axis.delete = False
    
    # Configure legend outside the plot area on the right
    from openpyxl.chart.legend import Legend
    legend = Legend()
    legend.position = 'r'  # Right position
    legend.overlay = False  # Place legend outside the plot area
    chart.legend = legend
    
    # Configure each series with colors
    for idx, series in enumerate(chart.series):
        series.smooth = True
        marker = Marker('circle')
        marker.size = MARKER_SIZE
        series.marker = marker
        
        color = user_colors[idx % len(user_colors)]
        line = LineProperties()
        line.width = LINE_WIDTH
        line.solidFill = color
        series.graphicalProperties.line = line
        
        if series.marker and series.marker.graphicalProperties:
            series.marker.graphicalProperties.solidFill = color
    
    return chart

def create_combined_chart(wb, all_activity_data, all_dates):
    """Create a combined chart showing all activity types"""
    ws_combined = wb.create_sheet("Combined Overview", 0)
    
    # Title
    ws_combined["A1"] = "Blueworks Live - All Activities Overview"
    ws_combined["A1"].font = Font(bold=True, size=16, color="2C3E50")
    ws_combined["A1"].alignment = Alignment(horizontal="left", vertical="center")
    ws_combined.merge_cells("A1:F1")
    ws_combined.row_dimensions[1].height = 30
    
    # Create data table for combined chart
    header_color = "2C3E50"
    ws_combined["A3"] = "Date"
    ws_combined["A3"].font = Font(bold=True, color="FFFFFF", size=12)
    ws_combined["A3"].fill = PatternFill(start_color=header_color, end_color=header_color, fill_type="solid")
    ws_combined["A3"].alignment = Alignment(horizontal="center", vertical="center")
    
    # Add activity type headers
    for idx, (activity_type, activity_name) in enumerate(ACTIVITY_TYPES.items(), start=2):
        cell = ws_combined.cell(row=3, column=idx)
        cell.value = activity_name
        cell.font = Font(bold=True, color="FFFFFF", size=12)
        cell.fill = PatternFill(start_color=header_color, end_color=header_color, fill_type="solid")
        cell.alignment = Alignment(horizontal="center", vertical="center")
    
    # Add data rows
    for row_idx, date in enumerate(all_dates, start=4):
        # Format date
        try:
            date_obj = datetime.strptime(date, "%Y-%m-%d")
            formatted_date = date_obj.strftime("%d %b")
        except:
            formatted_date = date
        
        ws_combined.cell(row=row_idx, column=1, value=formatted_date)
        
        # Add counts for each activity type
        for col_idx, activity_type in enumerate(ACTIVITY_TYPES.keys(), start=2):
            total_count = sum(all_activity_data[activity_type][date].values()) if date in all_activity_data[activity_type] else 0
            ws_combined.cell(row=row_idx, column=col_idx, value=total_count)
    
    # Create combined chart with specific dimensions
    # Height: 6.54 inches = 16.6 cm (rounded to 17)
    # Width: 13.8 inches = 35.05 cm (rounded to 35)
    chart = LineChart()
    chart.style = 13
    chart.title = "All Activities Over Time"
    chart.height = 17  # 6.54 inches
    chart.width = 40  # 13.8 inches
    chart.smooth = True
    
    data = Reference(ws_combined, min_col=2, min_row=3, max_row=len(all_dates) + 3, max_col=len(ACTIVITY_TYPES) + 1)
    cats = Reference(ws_combined, min_col=1, min_row=4, max_row=len(all_dates) + 3)
    
    chart.add_data(data, titles_from_data=True)
    chart.set_categories(cats)
    
    # Configure axes - set minimum to 0 to avoid negative values
    chart.y_axis.title = "Count"
    chart.y_axis.delete = False
    chart.y_axis.scaling.min = 0  # Prevent negative values on Y-axis
    chart.x_axis.title = ""
    chart.x_axis.delete = False
    
    # Add gridlines
    from openpyxl.chart.axis import ChartLines
    from openpyxl.chart.shapes import GraphicalProperties
    from openpyxl.drawing.line import LineProperties as DrawingLineProperties
    
    if chart.y_axis.majorGridlines is None:
        chart.y_axis.majorGridlines = ChartLines()
    
    gridline_props = GraphicalProperties()
    gridline_line = DrawingLineProperties()
    gridline_line.solidFill = "E0E0E0"
    gridline_line.width = 9525
    gridline_props.line = gridline_line
    chart.y_axis.majorGridlines.spPr = gridline_props
    
    # Configure legend outside the plot area at bottom
    from openpyxl.chart.legend import Legend
    legend = Legend()
    legend.position = 'b'  # Bottom position
    legend.overlay = False  # Place legend outside the plot area
    chart.legend = legend
    
    # Different colors for each activity type
    activity_colors = ["4472C4", "ED7D31", "70AD47", "FFC000"]
    
    for idx, series in enumerate(chart.series):
        series.smooth = True
        marker = Marker('circle')
        marker.size = 8
        series.marker = marker
        
        color = activity_colors[idx % len(activity_colors)]
        line = LineProperties()
        line.width = 28000
        line.solidFill = color
        series.graphicalProperties.line = line
        
        if series.marker and series.marker.graphicalProperties:
            series.marker.graphicalProperties.solidFill = color
    
    # Position chart to the right of the data table
    # Data table uses columns A through E (5 columns for 4 activity types + date)
    # Start chart at column G (column 7) to leave one column buffer
    chart_column = "G"
    chart_row = 3  # Start at same row as data table header
    ws_combined.add_chart(chart, f"{chart_column}{chart_row}")  # type: ignore
    
    # Adjust column widths
    for col_idx in range(1, len(ACTIVITY_TYPES) + 2):
        column_letter = get_column_letter(col_idx)
        ws_combined.column_dimensions[column_letter].width = 18

def main():
    """Main execution function with comprehensive error handling"""
    print("Blueworks Live All Activities Chart Generator")
    print("=" * 60)
    
    # Date range - use configuration values
    start_date_str = START_DATE
    end_date_str = END_DATE
    
    # Validate date range
    try:
        from dateutil import parser
        start_dt = parser.isoparse(start_date_str)
        end_dt = parser.isoparse(end_date_str)
        date_diff = (end_dt - start_dt).days
        
        # Extract display dates
        start_display = start_dt.strftime("%Y-%m-%d")
        end_display = end_dt.strftime("%Y-%m-%d")
        
        print(f"Fetching activity data from {start_display} to {end_display}")
        print(f"Date range: {date_diff} days")
        
        # Warn if exceeding API limit
        if date_diff > 31:
            print("\n⚠️  WARNING: Date range exceeds 31 days!")
            print(f"   Requested: {date_diff} days")
            print(f"   API Limit: 31 days maximum")
            print(f"   The API may reject this request or return incomplete data")
            print(f"   Consider splitting into multiple 31-day periods\n")
        elif date_diff < 0:
            print("\n❌ ERROR: End date is before start date!")
            print("=" * 60)
            return
        
    except ImportError:
        # dateutil not available, use simple parsing
        print(f"Fetching activity data from {start_date_str} to {end_date_str}")
        print("⚠️  Note: Install python-dateutil for date range validation")
    except Exception as e:
        print(f"⚠️  Warning: Could not validate date range: {e}")
        print(f"Fetching activity data from {start_date_str} to {end_date_str}")
    
    print("-" * 60)
    
    # Fetch all activity types
    all_activity_data = {}
    all_users_set = set()
    all_dates_set = set()
    failed_activities = []
    successful_activities = []
    
    for activity_type, activity_name in ACTIVITY_TYPES.items():
        api_response = fetch_activities(
            start_date_str,
            end_date_str,
            activity_type,
            ACCESS_TOKEN,
            API_BASE_URL,
            VERIFY_SSL
        )
        
        # Check if the API call had errors
        if "error" in api_response:
            failed_activities.append((activity_name, api_response.get("error")))
            print(f"  {activity_name}: ⚠️  Failed - {api_response.get('error')}")
        else:
            record_count = len(api_response.get('records', []))
            if record_count > 0:
                successful_activities.append(activity_name)
            print(f"  {activity_name}: ✓ {record_count} records")
        
        activity_counts = parse_activity_data(api_response)
        all_activity_data[activity_type] = activity_counts
        
        # Collect all users and dates
        for date, users in activity_counts.items():
            all_dates_set.add(date)
            all_users_set.update(users.keys())
    
    all_dates = sorted(all_dates_set)
    all_users = sorted(all_users_set)
    
    print("-" * 60)
    
    # Summary of API calls
    if failed_activities:
        print(f"\n⚠️  Warning: {len(failed_activities)} activity type(s) failed to fetch:")
        for activity_name, error in failed_activities:
            print(f"   - {activity_name}: {error}")
    
    if successful_activities:
        print(f"\n✓ Successfully fetched {len(successful_activities)} activity type(s):")
        for activity_name in successful_activities:
            print(f"   - {activity_name}")
    
    # Check if we have any data at all
    if not all_dates:
        print("\n" + "=" * 60)
        print("❌ ERROR: No activity data available!")
        print("=" * 60)
        print("\nPossible reasons:")
        print("  1. All API calls failed (check errors above)")
        print("  2. No activities occurred in the specified date range")
        print("  3. Authentication or permission issues")
        print("\nPlease check:")
        print("  - API_BASE_URL is correct")
        print("  - ACCESS_TOKEN is valid and not expired")
        print("  - Date range contains actual activity data")
        print("  - Network connectivity to the server")
        print("=" * 60)
        return
    
    if not all_users:
        print("\n⚠️  Warning: No user data found in the responses")
        print("   The Excel file will be created but may be empty")
    
    print(f"\nTotal unique users: {len(all_users)}")
    print(f"Total unique dates: {len(all_dates)}")
    print("-" * 60)
    
    # Create workbook with error handling
    try:
        wb = openpyxl.Workbook()
        if wb.active:
            wb.remove(wb.active)
    except Exception as e:
        print(f"\n❌ ERROR: Failed to create Excel workbook")
        print(f"   Error: {str(e)}")
        print("=" * 60)
        return
    
    # User colors
    user_colors = [
        "4472C4", "ED7D31", "A5A5A5", "FFC000",
        "5B9BD5", "70AD47", "264478", "9E480E",
        "636363", "997300"
    ]
    
    # Create combined overview first
    create_combined_chart(wb, all_activity_data, all_dates)
    
    # Create individual worksheets for each activity type
    for activity_type, activity_name in ACTIVITY_TYPES.items():
        activity_counts = all_activity_data[activity_type]
        
        # Create data worksheet
        ws_data = create_data_worksheet(
            wb,
            f"{activity_name} Data",
            activity_counts,
            all_users,
            all_dates
        )
        
        # Create dashboard worksheet
        ws_dashboard = wb.create_sheet(f"{activity_name} Dashboard")
        
        # Title
        ws_dashboard["A1"] = f"Blueworks Live - {activity_name}"
        ws_dashboard["A1"].font = Font(bold=True, size=16, color="2C3E50")
        ws_dashboard["A1"].alignment = Alignment(horizontal="left", vertical="center")
        ws_dashboard.merge_cells("A1:F1")
        ws_dashboard.row_dimensions[1].height = 30
        
        # Summary statistics - improved tabular format
        ws_dashboard["A3"] = "Summary Statistics"
        ws_dashboard["A3"].font = Font(bold=True, size=14, color="FFFFFF")
        ws_dashboard["A3"].fill = PatternFill(start_color="2C3E50", end_color="2C3E50", fill_type="solid")
        ws_dashboard["A3"].alignment = Alignment(horizontal="center", vertical="center")
        ws_dashboard.merge_cells("A3:B3")
        ws_dashboard.row_dimensions[3].height = 25
        
        total_activities = sum(sum(users.values()) for users in activity_counts.values())
        
        # Format date range nicely
        date_range_str = "N/A"
        if all_dates:
            try:
                start_date_obj = datetime.strptime(all_dates[0], "%Y-%m-%d")
                end_date_obj = datetime.strptime(all_dates[-1], "%Y-%m-%d")
                date_range_str = f"{start_date_obj.strftime('%d %b %Y')} to {end_date_obj.strftime('%d %b %Y')}"
            except:
                date_range_str = f"{all_dates[0]} to {all_dates[-1]}"
        
        summary_data = [
            ["Total Events", total_activities],
            ["Unique Users", len([u for u in all_users if any(activity_counts[d].get(u, 0) > 0 for d in all_dates)])],
            ["Date Range", date_range_str],
            ["Total Days", len(all_dates)]
        ]
        
        # Create bordered table with alternating row colors
        header_color = "34495E"
        alt_row_color = "ECF0F1"
        border_style = Border(
            left=Side(style='thin', color='95A5A6'),
            right=Side(style='thin', color='95A5A6'),
            top=Side(style='thin', color='95A5A6'),
            bottom=Side(style='thin', color='95A5A6')
        )
        
        for idx, (label, value) in enumerate(summary_data, start=4):
            # Label cell
            label_cell = ws_dashboard.cell(row=idx, column=1, value=label)
            label_cell.font = Font(bold=True, size=11, color="FFFFFF")
            label_cell.fill = PatternFill(start_color=header_color, end_color=header_color, fill_type="solid")
            label_cell.alignment = Alignment(horizontal="left", vertical="center")
            label_cell.border = border_style
            
            # Value cell with alternating colors
            value_cell = ws_dashboard.cell(row=idx, column=2, value=value)
            value_cell.font = Font(size=11, bold=True)
            value_cell.fill = PatternFill(start_color=alt_row_color, end_color=alt_row_color, fill_type="solid")
            value_cell.alignment = Alignment(horizontal="center", vertical="center")
            value_cell.border = border_style
            
            # Set row height for better spacing
            ws_dashboard.row_dimensions[idx].height = 22
        
        # Create chart
        chart = create_chart(ws_data, all_users, all_dates, f"{activity_name} Over Time", user_colors)
        ws_dashboard.add_chart(chart, "A10")  # type: ignore
        
        # Adjust column widths
        ws_dashboard.column_dimensions['A'].width = 25
        ws_dashboard.column_dimensions['B'].width = 20
    
    # Save workbook with error handling
    output_file = OUTPUT_FILENAME
    try:
        wb.save(output_file)
        print(f"\n✓ Excel file created successfully: {output_file}")
        print("=" * 60)
    except PermissionError:
        print(f"\n❌ ERROR: Permission denied when saving {output_file}")
        print(f"   The file may be open in another application")
        print(f"   Please close the file and try again")
        print("=" * 60)
        return
    except OSError as e:
        print(f"\n❌ ERROR: Failed to save Excel file")
        print(f"   Error: {str(e)}")
        print(f"   Check disk space and file permissions")
        print("=" * 60)
        return
    except Exception as e:
        print(f"\n❌ ERROR: Unexpected error while saving Excel file")
        print(f"   Error: {str(e)}")
        print("=" * 60)
        return

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\n\n⚠️  Operation cancelled by user")
        print("=" * 60)
        sys.exit(0)
    except Exception as e:
        print(f"\n❌ FATAL ERROR: Unexpected error occurred")
        print(f"   Error: {str(e)}")
        print(f"   Type: {type(e).__name__}")
        print("=" * 60)
        sys.exit(1)
1 comment
115 views

Permalink

Comments

05/14/26 09:17 AM

Thank you for this insightful and well-structured article!