Patabugen / Sunsama Daily Shutdownn Today's Progress Generator

// ==UserScript==
// @name         Sunsama Daily Shutdownn Today's Progress Generator
// @namespace    https://www.patabugen.co.uk/
// @version      1.2
// @description  Extract and display today's task progress in Sunsama. Roughly mimicks the pre-AI Daily Shutdown routine.
// @author       Sami Walbury
// @match        https://app.sunsama.com/group/*/shutdownDay/reflect*
// @grant        none
// @license      MIT
// @run-at       document-idle
// @downloadURL  https://openuserjs.org/install/Patabugen/Sunsama_Daily_Shutdownn_Todays_Progress_Generator.user.js
// @updateURL    https://openuserjs.org/meta/Patabugen/Sunsama_Daily_Shutdownn_Todays_Progress_Generator.meta.js
// @copyright    2025, Patabugen (https://openuserjs.org/users/Patabugen)
// ==/UserScript==

/*jshint esversion: 6 */

/**
 * Changelog:
 * 1.0 Initial Release
 * 1.1 Escape HTML in task titles and added tags for auto update and set jshint to esversion 6.
 * 1.2 Don't set the background colour of the summary to fix display in dark mode
 **/
(function () {
  'use strict';

  function waitForElement(selector, callback) {
    const el = document.querySelector(selector);
    if (el) {
      callback(el);
    }
    else {
      const observer = new MutationObserver(() => {
        const el = document.querySelector(selector);
        if (el) {
          observer.disconnect();
          callback(el);
        }
      });
      observer.observe(document.body, {
        childList: true,
        subtree: true
      });
    }
  }

  function extractTasks() {
    const firstDayContainer = document.querySelector('.kanban-day-tasks-items-container');
    if (!firstDayContainer) return;

    const taskContainers = firstDayContainer.querySelectorAll('.kanban-task-container.kanban-task-container-draggable');
    const groupedTasks = {};

    taskContainers.forEach(task => {
      const titleElement = task.querySelector('.task-item-description');
      const groupElement = task.querySelector('.kanban-task-stream-label');
      if (!titleElement || !groupElement) return;

      const taskTitle = titleElement.textContent.trim();
      const groupTitle = groupElement.textContent.trim();
      const subtaskElements = task.querySelectorAll('.subtask-item-description-container');

       // Escape HTML in task names using an HTMLOptionElement: https://stackoverflow.com/a/22706073/1625144
      const subtaskTexts = Array.from(subtaskElements).map(s => (new Option(s.textContent.trim())).innerHTML);

      if (!groupedTasks[groupTitle]) {
        groupedTasks[groupTitle] = [];
      }

      groupedTasks[groupTitle].push({
        title: (new Option(taskTitle)).innerHTML,
        subtasks: subtaskTexts
      });
    });

    let output = '<h3>Today\'s Progress:</h3><br>';

    for (const [group, tasks] of Object.entries(groupedTasks)) {
      if (group == 'personal') {
          continue;
      }
      output += `<h4><strong>${group}</strong></h4>`;
      output += '<ul>';

      tasks.forEach(task => {
        output += `<li>${task.title}<br>`;
        if (task.subtasks.length > 0) {
            output += '<ul>';

            task.subtasks.forEach(sub => {
                output += `<li>${sub}</li>`;
            });
            output += '</ul>';
        }
        output += '</li>';
      });

      output += '</ul><br>';
    }

    displayRichText(output);
  }

  function getInjectedContainer() {
    const groupBodyContainer = document.querySelector('#group-body-container');
    if (!groupBodyContainer) return;

    // Remove an existing container if it exists
    let injectecdContainer = groupBodyContainer.querySelector('#injected-task-summary');
    if (!injectecdContainer) {
      injectecdContainer = document.createElement('div');
      injectecdContainer.className = 'kanban-day-container'
      injectecdContainer.id = 'injected-task-summary';
      injectecdContainer.style.overflow = 'auto';
      injectecdContainer.style.maxWidth = '100%';
      injectecdContainer.style.boxSizing = 'border-box';
      injectecdContainer.style.order = 10; // Put it at the far right of any columns

      let title = document.createElement('div')
      title.className = "kanban-day-label-container"
      title.innerHTML = 'Status Update';
      injectecdContainer.appendChild(title);

      let spacer = document.createElement('div');

      let body = document.createElement('div');
      body.className = 'today-progress-body';
      injectecdContainer.appendChild(body);

    }

    groupBodyContainer.appendChild(injectecdContainer);

    return injectecdContainer.querySelector('.today-progress-body');
  }

  function displayRichText(htmlContent) {
    const richTextEditor = document.createElement('div');
    richTextEditor.contentEditable = 'true';
    richTextEditor.style.width = '100%';
    richTextEditor.style.minHeight = '200px';
    richTextEditor.style.border = '1px solid #ccc';
    richTextEditor.style.padding = '10px';
    richTextEditor.style.boxSizing = 'border-box';
    richTextEditor.innerHTML = htmlContent;

    const injectedContainer = getInjectedContainer();
    injectedContainer.replaceChildren(richTextEditor);
  }

  function injectButton() {
    const button = document.createElement('div');
    button.className = 'plan-day-btn';
    button.textContent = "Generate Today's Progress";
    button.style.cursor = 'pointer';
    button.style.padding = '10px';
    button.style.textAlign = 'center';
    button.style.borderRadius = '4px';
    button.style.maxHeight = '40px';
    button.style.overflow = 'hidden';
    button.style.whiteSpace = 'nowrap';
    button.style.textOverflow = 'ellipsis';

    button.addEventListener('click', extractTasks);

    const injectedContainer = getInjectedContainer();
    injectedContainer.replaceChildren(button);
  }

  waitForElement('.kanban-day-container', injectButton);
})();