User:Polygnotus/Scripts/Backlog.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump.
This code will be executed when previewing this page.
This code will be executed when previewing this page.
Documentation for this user script can be added at User:Polygnotus/Scripts/Backlog.
/**
* Wikipedia Task Manager for common.js - Sidebar Version
* Add this to your User:YourUsername/common.js page
* A "Tasks" tab appears next to Article/Talk tabs - click to activate
*/
$(document).ready(function() {
const TASK_PAGE = 'User:Polygnotus/barfoo2';
class WikiTaskManager {
constructor() {
this.tasks = [];
this.currentIndex = 0;
this.pageContent = '';
this.baseTimestamp = '';
this.sidebarWidth = localStorage.getItem('task_sidebar_width') || '450px';
this.isVisible = localStorage.getItem('task_sidebar_visible') !== 'false';
this.isActive = localStorage.getItem('task_manager_active') === 'true';
this.init();
}
async init() {
// Create tab on article pages
this.createTaskTab();
// Check if task manager is active
if (this.isActive) {
console.log('Task manager active - creating UI and loading from storage');
this.createUI();
this.loadTasksFromStorage();
}
}
createTaskTab() {
// Only add tab on article pages (namespace 0) or if already active
if (mw.config.get('wgNamespaceNumber') === 0 || this.isActive) {
let portletId = 'p-namespaces';
if (mw.config.get('skin') === 'vector-2022') {
portletId = 'p-associated-pages';
}
const tabText = this.isActive ? 'Tasks ✓' : 'Tasks';
const tabTitle = this.isActive ? 'Task manager active (click to toggle sidebar)' : 'Start task manager';
const taskLink = mw.util.addPortletLink(
portletId,
'#',
tabText,
'ca-tasks',
tabTitle,
't'
);
taskLink.addEventListener('click', (e) => {
e.preventDefault();
if (this.isActive) {
// Toggle sidebar visibility
this.toggleSidebar();
} else {
// Activate task manager
this.activateTaskManager();
}
});
}
}
async activateTaskManager() {
localStorage.setItem('task_manager_active', 'true');
localStorage.setItem('task_sidebar_visible', 'true');
this.isActive = true;
this.isVisible = true;
// Create UI and load tasks
this.createUI();
await this.loadTasks();
// Reload to update the tab
location.reload();
}
createUI() {
console.log('Creating UI...');
const sidebar = document.createElement('div');
sidebar.id = 'task-manager-sidebar';
sidebar.innerHTML = `
<div id="task-sidebar-header">
<h3>Task Manager</h3>
<div id="task-sidebar-controls">
<button id="task-close-btn" title="Hide sidebar">−</button>
</div>
</div>
<div id="task-sidebar-content">
<div id="task-info-section">
<div id="task-counter">Loading...</div>
<div id="task-article-info">
<strong id="task-article-title">...</strong>
<div id="task-article-url-container">
<a id="task-article-url" href="#" target="_blank">Open article in new tab →</a>
</div>
</div>
</div>
<div id="task-instructions-section">
<h4>Instructions</h4>
<pre id="task-instructions-text">Loading tasks...</pre>
</div>
<div id="task-controls-section">
<button class="task-btn task-btn-done" id="task-btn-done" disabled>✓ Done</button>
<button class="task-btn task-btn-next" id="task-btn-next" disabled>Next →</button>
<button class="task-btn task-btn-refresh" id="task-btn-refresh">↻ Refresh</button>
<button class="task-btn task-btn-storage" id="task-btn-storage">📊 Storage Info</button>
<button class="task-btn task-btn-deactivate" id="task-btn-deactivate">✕ Close Task Manager</button>
</div>
</div>
<div id="task-resize-handle"></div>
`;
this.createStyles();
console.log('Appending sidebar to body...');
document.body.appendChild(sidebar);
console.log('Sidebar appended. Element exists:', !!document.getElementById('task-manager-sidebar'));
this.attachEventListeners();
this.makeResizable();
if (!this.isVisible) {
console.log('Hiding sidebar (isVisible is false)');
this.hideSidebar();
} else {
console.log('Sidebar should be visible');
}
}
createStyles() {
const style = document.createElement('style');
style.textContent = `
#task-manager-sidebar {
position: fixed;
top: 0;
right: 0;
width: ${this.sidebarWidth};
height: 100vh;
background: #fff;
border-left: 2px solid #0645ad;
box-shadow: -2px 0 8px rgba(0,0,0,0.1);
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
}
#task-manager-sidebar.hidden {
transform: translateX(100%);
}
#task-sidebar-header {
background: #0645ad;
color: white;
padding: 12px 15px;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
#task-sidebar-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
#task-close-btn {
background: none;
border: none;
color: white;
font-size: 24px;
line-height: 20px;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
#task-close-btn:hover {
opacity: 0.8;
}
#task-sidebar-content {
padding: 20px;
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 20px;
}
#task-info-section {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
border: 1px solid #ddd;
}
#task-counter {
font-size: 13px;
color: #666;
margin-bottom: 10px;
font-weight: 500;
}
#task-article-info {
margin-top: 10px;
}
#task-article-title {
font-size: 16px;
color: #333;
display: block;
margin-bottom: 8px;
}
#task-article-url-container {
margin-top: 8px;
}
#task-article-url {
color: #0645ad;
text-decoration: none;
font-size: 13px;
display: inline-block;
}
#task-article-url:hover {
text-decoration: underline;
}
#task-instructions-section {
flex: 1;
min-height: 200px;
display: flex;
flex-direction: column;
}
#task-instructions-section h4 {
margin: 0 0 10px 0;
font-size: 14px;
font-weight: 600;
color: #333;
}
#task-instructions-text {
white-space: pre-wrap;
word-wrap: break-word;
font-size: 13px;
line-height: 1.6;
font-family: 'Courier New', monospace;
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
border: 1px solid #ddd;
margin: 0;
flex: 1;
overflow-y: auto;
}
#task-controls-section {
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 10px;
border-top: 1px solid #ddd;
}
.task-btn {
padding: 12px 20px;
font-size: 14px;
font-weight: 500;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s, opacity 0.2s;
width: 100%;
}
.task-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.task-btn-done {
background: #28a745;
color: white;
}
.task-btn-done:hover:not(:disabled) {
background: #218838;
}
.task-btn-next {
background: #007bff;
color: white;
}
.task-btn-next:hover:not(:disabled) {
background: #0056b3;
}
.task-btn-refresh {
background: #6c757d;
color: white;
}
.task-btn-refresh:hover:not(:disabled) {
background: #5a6268;
}
.task-btn-storage {
background: #17a2b8;
color: white;
font-size: 13px;
}
.task-btn-storage:hover:not(:disabled) {
background: #138496;
}
.task-btn-deactivate {
background: #dc3545;
color: white;
margin-top: 10px;
font-size: 13px;
}
.task-btn-deactivate:hover:not(:disabled) {
background: #c82333;
}
#task-resize-handle {
position: absolute;
left: 0;
top: 0;
width: 5px;
height: 100%;
cursor: ew-resize;
background: transparent;
}
#task-resize-handle:hover {
background: rgba(6, 69, 173, 0.2);
}
.task-error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 6px;
border: 1px solid #f5c6cb;
}
.task-success {
background: #d4edda;
color: #155724;
padding: 15px;
border-radius: 6px;
border: 1px solid #c3e6cb;
}
body {
margin-right: ${this.sidebarWidth};
transition: margin-right 0.3s ease;
}
body.task-sidebar-hidden {
margin-right: 0;
}
`;
document.head.appendChild(style);
this.styleElement = style;
}
attachEventListeners() {
document.getElementById('task-btn-done').addEventListener('click', () => this.markTaskDone());
document.getElementById('task-btn-next').addEventListener('click', () => this.nextTask());
document.getElementById('task-btn-refresh').addEventListener('click', () => this.loadTasks());
document.getElementById('task-close-btn').addEventListener('click', () => this.toggleSidebar());
document.getElementById('task-btn-storage').addEventListener('click', () => this.showStorageInfo());
document.getElementById('task-btn-deactivate').addEventListener('click', () => this.deactivateTaskManager());
}
showStorageInfo() {
let total = 0;
let itemCount = 0;
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
total += localStorage[key].length + key.length;
itemCount++;
}
}
const taskData = localStorage.getItem('task_manager_data');
const taskSize = taskData ? taskData.length : 0;
const info = `Storage Usage:
━━━━━━━━━━━━━━━━━━━━━━
Total localStorage: ${(total / 1024).toFixed(2)} KB
Task Manager data: ${(taskSize / 1024).toFixed(2)} KB
Total items: ${itemCount}
Limit: ~5-10 MB (browser dependent)
Used: ${((total / (5 * 1024 * 1024)) * 100).toFixed(1)}%`;
alert(info);
}
deactivateTaskManager() {
if (confirm('Close task manager? You can restart it by clicking "Start Tasks" tab')) {
localStorage.setItem('task_manager_active', 'false');
localStorage.removeItem('task_manager_data');
localStorage.setItem('task_sidebar_visible', 'false');
location.reload();
}
}
makeResizable() {
const handle = document.getElementById('task-resize-handle');
const sidebar = document.getElementById('task-manager-sidebar');
let isResizing = false;
handle.addEventListener('mousedown', (e) => {
isResizing = true;
document.body.style.cursor = 'ew-resize';
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const newWidth = window.innerWidth - e.clientX;
if (newWidth >= 300 && newWidth <= 800) {
this.sidebarWidth = newWidth + 'px';
sidebar.style.width = this.sidebarWidth;
document.body.style.marginRight = this.sidebarWidth;
this.styleElement.textContent = this.styleElement.textContent.replace(
/width: \d+px;/g,
`width: ${this.sidebarWidth};`
).replace(
/margin-right: \d+px;/g,
`margin-right: ${this.sidebarWidth};`
);
localStorage.setItem('task_sidebar_width', this.sidebarWidth);
}
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
});
}
toggleSidebar() {
const sidebar = document.getElementById('task-manager-sidebar');
this.isVisible = !this.isVisible;
if (this.isVisible) {
sidebar.classList.remove('hidden');
document.body.classList.remove('task-sidebar-hidden');
} else {
sidebar.classList.add('hidden');
document.body.classList.add('task-sidebar-hidden');
}
localStorage.setItem('task_sidebar_visible', this.isVisible);
}
hideSidebar() {
document.getElementById('task-manager-sidebar').classList.add('hidden');
document.body.classList.add('task-sidebar-hidden');
}
async loadTasks() {
try {
document.getElementById('task-btn-done').disabled = true;
document.getElementById('task-btn-next').disabled = true;
document.getElementById('task-btn-refresh').disabled = true;
document.getElementById('task-instructions-text').textContent = 'Loading tasks from Wikipedia...';
const data = await this.fetchWikiPage(TASK_PAGE);
this.pageContent = data.content;
this.baseTimestamp = data.timestamp;
this.tasks = this.parseTasks(this.pageContent);
if (this.tasks.length === 0) {
this.showError('No tasks found on the page');
return;
}
this.currentIndex = 0;
this.saveTasksToStorage();
this.displayCurrentTask();
document.getElementById('task-btn-refresh').disabled = false;
} catch (error) {
this.showError('Error loading tasks: ' + error.message);
document.getElementById('task-btn-refresh').disabled = false;
}
}
loadTasksFromStorage() {
console.log('Loading tasks from storage...');
const storedData = localStorage.getItem('task_manager_data');
console.log('Stored data:', storedData ? 'Found' : 'Not found');
if (storedData) {
try {
const data = JSON.parse(storedData);
this.tasks = data.tasks || [];
this.currentIndex = data.currentIndex || 0;
this.baseTimestamp = data.baseTimestamp || '';
console.log('Loaded tasks:', this.tasks.length, 'Current index:', this.currentIndex);
if (this.tasks.length > 0) {
this.displayCurrentTask();
const refreshBtn = document.getElementById('task-btn-refresh');
if (refreshBtn) refreshBtn.disabled = false;
} else {
const instructionsText = document.getElementById('task-instructions-text');
if (instructionsText) {
instructionsText.textContent = 'No tasks loaded. Click Refresh to load tasks.';
}
}
} catch (e) {
console.error('Error parsing stored tasks:', e);
const instructionsText = document.getElementById('task-instructions-text');
if (instructionsText) {
instructionsText.textContent = 'Error loading saved tasks. Click Refresh to reload.';
}
}
} else {
const instructionsText = document.getElementById('task-instructions-text');
if (instructionsText) {
instructionsText.textContent = 'No tasks loaded. Click Refresh to load tasks.';
}
}
}
saveTasksToStorage() {
try {
// Don't store pageContent - it's too large and not needed
const data = {
tasks: this.tasks,
currentIndex: this.currentIndex,
baseTimestamp: this.baseTimestamp
};
localStorage.setItem('task_manager_data', JSON.stringify(data));
} catch (e) {
// If quota exceeded, try to store minimal data
if (e.name === 'QuotaExceededError') {
console.warn('LocalStorage quota exceeded, storing minimal data');
try {
const minimalData = {
tasks: this.tasks.slice(0, 3), // Only store first 3 tasks
currentIndex: this.currentIndex,
baseTimestamp: this.baseTimestamp
};
localStorage.setItem('task_manager_data', JSON.stringify(minimalData));
} catch (e2) {
console.error('Failed to save even minimal task data:', e2);
}
}
}
}
async fetchWikiPage(pageTitle) {
const url = `/w/api.php?action=query&titles=${encodeURIComponent(pageTitle)}&prop=revisions&rvprop=content|timestamp&format=json&formatversion=2`;
const username = mw.config.get('wgUserName') || 'Anonymous';
const response = await fetch(url, {
headers: {
'Api-User-Agent': `WikiTaskManager/1.0 (User:${username})`
}
});
const data = await response.json();
const page = data.query.pages[0];
if (page.missing) {
throw new Error('Page not found');
}
return {
content: page.revisions[0].content,
timestamp: page.revisions[0].timestamp
};
}
parseTasks(content) {
const tasks = content.split('================================================================================');
return tasks
.map(task => task.trim())
.filter(task => task.length > 0 && !task.startsWith('__NOTOC__'));
}
parseTask(taskLine) {
const match = taskLine.match(/^Article: (.+?)\nURL: (.+?)\n\nCLAUDE'S ANALYSIS:\n([\s\S]+)$/);
if (!match) {
return {
title: 'Unknown',
url: '',
instructions: taskLine
};
}
return {
title: match[1].trim(),
url: match[2].trim(),
instructions: match[3].trim()
};
}
displayCurrentTask() {
if (this.currentIndex >= this.tasks.length) {
this.showCompletion();
return;
}
const taskLine = this.tasks[this.currentIndex];
const task = this.parseTask(taskLine);
console.log('Displaying task:', {
index: this.currentIndex,
title: task.title,
url: task.url
});
document.getElementById('task-counter').textContent = `Task ${this.currentIndex + 1} of ${this.tasks.length}`;
document.getElementById('task-instructions-text').textContent = task.instructions;
document.getElementById('task-article-title').textContent = task.title;
document.getElementById('task-article-url').textContent = task.url;
document.getElementById('task-article-url').href = task.url;
document.getElementById('task-btn-done').disabled = false;
document.getElementById('task-btn-next').disabled = this.currentIndex >= this.tasks.length - 1;
this.saveTasksToStorage();
// Check if we need to navigate to the article
// Extract the page title from the task URL
const urlMatch = task.url.match(/\/wiki\/(.+)$/);
const targetPage = urlMatch ? decodeURIComponent(urlMatch[1]) : '';
const currentPage = mw.config.get('wgPageName');
console.log('Navigation check:', {
targetPage: targetPage,
currentPage: currentPage,
needsNavigation: targetPage && currentPage !== targetPage
});
// Only navigate if we're not already on the target page
if (targetPage && currentPage !== targetPage) {
console.log('Navigating to:', task.url);
window.location.href = task.url;
}
}
async markTaskDone() {
document.getElementById('task-btn-done').disabled = true;
document.getElementById('task-btn-next').disabled = true;
try {
const taskToRemove = this.tasks[this.currentIndex];
// Remove task from local array
this.tasks.splice(this.currentIndex, 1);
// Rebuild page content
const newContent = this.tasks.join('\n================================================================================\n') +
(this.tasks.length > 0 ? '\n================================================================================\n' : '');
// Save to Wikipedia
await this.saveWikiPage(TASK_PAGE, newContent, 'Task completed and removed');
// Save state
this.saveTasksToStorage();
// Display next task
if (this.tasks.length === 0) {
this.showCompletion();
} else {
if (this.currentIndex >= this.tasks.length) {
this.currentIndex = this.tasks.length - 1;
}
this.displayCurrentTask();
}
} catch (error) {
this.showError('Error marking task as done: ' + error.message);
// Reload tasks to get current state
await this.loadTasks();
}
}
async saveWikiPage(pageTitle, content, summary) {
const username = mw.config.get('wgUserName') || 'Anonymous';
const userAgent = `WikiTaskManager/1.0 (User:${username})`;
// Get edit token
const tokenUrl = `/w/api.php?action=query&meta=tokens&format=json&formatversion=2`;
const tokenResponse = await fetch(tokenUrl, {
headers: {
'Api-User-Agent': userAgent
}
});
const tokenData = await tokenResponse.json();
const csrfToken = tokenData.query.tokens.csrftoken;
// Edit page
const editUrl = `/w/api.php`;
const formData = new FormData();
formData.append('action', 'edit');
formData.append('title', pageTitle);
formData.append('text', content);
formData.append('summary', summary);
formData.append('token', csrfToken);
formData.append('format', 'json');
formData.append('basetimestamp', this.baseTimestamp);
const editResponse = await fetch(editUrl, {
method: 'POST',
headers: {
'Api-User-Agent': userAgent
},
body: formData
});
const editData = await editResponse.json();
if (editData.error) {
throw new Error(editData.error.info);
}
if (editData.edit && editData.edit.result !== 'Success') {
throw new Error('Edit failed: ' + JSON.stringify(editData.edit));
}
// Update base timestamp for next edit
this.baseTimestamp = editData.edit.newtimestamp;
this.saveTasksToStorage();
}
nextTask() {
if (this.currentIndex < this.tasks.length - 1) {
this.currentIndex++;
this.saveTasksToStorage();
this.displayCurrentTask();
}
}
showCompletion() {
document.getElementById('task-instructions-text').innerHTML = '<div class="task-success">All tasks completed!</div>';
document.getElementById('task-counter').textContent = 'No tasks remaining';
document.getElementById('task-article-title').textContent = 'All done';
document.getElementById('task-article-url').textContent = '';
document.getElementById('task-article-url').href = '#';
document.getElementById('task-btn-done').disabled = true;
document.getElementById('task-btn-next').disabled = true;
}
showError(message) {
document.getElementById('task-instructions-text').innerHTML = `<div class="task-error">${message}</div>`;
document.getElementById('task-btn-done').disabled = true;
document.getElementById('task-btn-next').disabled = true;
}
}
// Initialize the task manager
new WikiTaskManager();
});