Last active
March 16, 2026 01:37
-
-
Save hiyukoim/121273fbded5f1d4ad6d8fca312a1835 to your computer and use it in GitHub Desktop.
Userscript for Tasks.md: Adds a board switcher, n8n sync button, and automatic today/overdue alerts. (https://github.com/BaldissaraMatheus/Tasks.md)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Tasks.md Plus | |
| // @description Adds a board switcher with background due date alerts and a customizable webhook sync. | |
| // @match https://YOUR-TASKS-DOMAIN-HERE.com/* | |
| // @version 1.4 | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @grant GM_registerMenuCommand | |
| // @updateURL https://gist.githubusercontent.com/hiyukoim/121273fbded5f1d4ad6d8fca312a1835/raw/Tasks.md%2520Plus | |
| // @downloadURL https://gist.githubusercontent.com/hiyukoim/121273fbded5f1d4ad6d8fca312a1835/raw/Tasks.md%2520Plus | |
| // ==/UserScript== | |
| (function() { | |
| const WEBHOOK_STORAGE_KEY = 'WebhookUrl'; | |
| // --- Register Menu Command for the generic Webhook --- | |
| GM_registerMenuCommand('βοΈ Set Webhook URL', () => { | |
| const currentUrl = GM_getValue(WEBHOOK_STORAGE_KEY, ''); | |
| const newUrl = prompt('Enter your Webhook URL for Syncing:\n(e.g., n8n, Zapier, or custom endpoint)', currentUrl); | |
| if (newUrl !== null) { | |
| GM_setValue(WEBHOOK_STORAGE_KEY, newUrl.trim()); | |
| alert('β Webhook URL saved successfully!'); | |
| } | |
| }); | |
| function getTodayStr() { | |
| const today = new Date(); | |
| return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; | |
| } | |
| // --- Core: Scan Board via /_api/resource/{board}/ --- | |
| async function getBoardStatus(boardName) { | |
| const activeLanes = new Set(['_To-Do', 'Doing']); | |
| const todayStr = getTodayStr(); | |
| let boardStatus = 'none'; | |
| try { | |
| const res = await fetch(`/_api/resource/${encodeURIComponent(boardName)}/`); | |
| if (!res.ok) return 'none'; | |
| const lanes = await res.json(); | |
| for (const lane of lanes) { | |
| if (!activeLanes.has(lane.name)) continue; | |
| for (const file of lane.files) { | |
| const matches = [...(file.content || '').matchAll(/\[due:(\d{4}-\d{2}-\d{2})\]/g)]; | |
| for (const match of matches) { | |
| const dueDate = match[1]; | |
| if (dueDate < todayStr) return 'overdue'; | |
| else if (dueDate === todayStr) boardStatus = 'today'; | |
| } | |
| } | |
| } | |
| } catch (e) { | |
| console.error(`[Tasks.md Plus] Failed to scan board: ${boardName}`, e); | |
| } | |
| return boardStatus; | |
| } | |
| // --- Title Indicator (π₯ / β) --- | |
| function injectTitleIcon(status) { | |
| const titleEl = document.querySelector('h1.app-title'); | |
| if (!titleEl) return false; | |
| const existing = titleEl.querySelector('.tm-plus-status'); | |
| if (existing) existing.remove(); | |
| if (status === 'none') return true; | |
| const icon = document.createElement('span'); | |
| icon.className = 'tm-plus-status'; | |
| icon.textContent = status === 'overdue' ? 'π₯ ' : 'β '; | |
| titleEl.prepend(icon); | |
| return true; | |
| } | |
| function waitForTitleAndInject(status) { | |
| if (injectTitleIcon(status)) return; | |
| const observer = new MutationObserver(() => { | |
| if (injectTitleIcon(status)) observer.disconnect(); | |
| }); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| } | |
| // --- Main UI Initialization --- | |
| async function init() { | |
| const res = await fetch('/_api/resource/'); | |
| const boards = await res.json(); | |
| const currentBoard = boards.find(b => location.pathname.includes(b.name)); | |
| // --- Create Board Switcher (Native Structure) --- | |
| const groupItem = document.createElement('div'); | |
| groupItem.className = 'app-header__group-item'; | |
| groupItem.style.marginLeft = '8px'; | |
| const label = document.createElement('div'); | |
| label.className = 'app-header__group-item-label'; | |
| label.textContent = 'Board:'; | |
| const select = document.createElement('select'); | |
| boards.forEach(board => { | |
| const opt = document.createElement('option'); | |
| opt.value = board.name; | |
| const cleanName = board.name.replaceAll('_', ' '); | |
| opt.textContent = `β³ ${cleanName}`; | |
| const isCurrentBoard = currentBoard && board.name === currentBoard.name; | |
| if (isCurrentBoard) opt.selected = true; | |
| select.appendChild(opt); | |
| getBoardStatus(board.name).then(status => { | |
| if (status === 'overdue') opt.textContent = `π₯ ${cleanName}`; | |
| else if (status === 'today') opt.textContent = `β ${cleanName}`; | |
| else opt.textContent = cleanName; | |
| if (isCurrentBoard) waitForTitleAndInject(status); | |
| }); | |
| }); | |
| select.onchange = () => location.href = `/${select.value}`; | |
| groupItem.appendChild(label); | |
| groupItem.appendChild(select); | |
| // --- Create Sync Button (Native Style) --- | |
| const syncBtn = document.createElement('button'); | |
| syncBtn.type = 'button'; | |
| syncBtn.textContent = 'π Sync'; | |
| syncBtn.style.marginLeft = '8px'; | |
| syncBtn.onclick = async () => { | |
| const url = GM_getValue(WEBHOOK_STORAGE_KEY, ''); | |
| if (!url) return alert('Please set your Webhook URL in the Tampermonkey Menu first!'); | |
| const originalText = syncBtn.textContent; | |
| syncBtn.textContent = 'β³'; | |
| await fetch(url, { mode: 'no-cors' }).catch(() => {}); | |
| syncBtn.textContent = 'β '; | |
| setTimeout(() => syncBtn.textContent = originalText, 2000); | |
| }; | |
| const injectUI = () => { | |
| const header = document.querySelector('.app-header'); | |
| if (header) { | |
| // Places the new UI at the end of the header (after Language) | |
| header.appendChild(groupItem); | |
| header.appendChild(syncBtn); | |
| return true; | |
| } | |
| return false; | |
| }; | |
| if (!injectUI()) { | |
| const headerObserver = new MutationObserver(() => { | |
| if (injectUI()) headerObserver.disconnect(); | |
| }); | |
| headerObserver.observe(document.body, { childList: true, subtree: true }); | |
| } | |
| } | |
| // Start | |
| if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); | |
| else init(); | |
| })(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
π Tasks.md Plus
Enhance your [Tasks.md](https://github.com/BaldissaraMatheus/Tasks.md) experience with a native-feeling header dashboard. This userscript adds a global board switcher with real-time due date alerts and a customizable webhook sync button.
β¨ Features
Global Board Switcher: Navigate between all your boards directly from the header.
Smart Indicators: Scans background folders automatically.
π₯ Overdue: Tasks that passed their due date.
β Today: Tasks due today.
Webhook Integration: A "Sync" button to trigger n8n, Zapier, or any custom webhook to keep your files in sync.
Theme Native: Automatically inherits your Tasks.md CSS theme for a seamless look.
π οΈ How to Use
1. Install a Userscript Manager
Install [Tampermonkey](https://www.tampermonkey.net/) (recommended). While it may work with other managers like Violentmonkey or GreaseMonkey, it has been primarily tested on Tampermonkey.
2. Configure Your Instance
To protect your privacy, the script uses a placeholder for the URL. You must tell Tampermonkey where to run:
https://task.your-domain.com/*.3. Set Up Your Webhook
The Sync button allows you to push your local board state to external services like n8n: