Organize tasks by priority and due dates. Changes sync two-way with Google Sheets.
Total Tasks
0
Completed
0
Pending
0
Overdue
0
Done
Task Name
Category
Priority
Due Date
Actions
Workouts & Progressive Overload
Track your lifts and let Gemini AI calculate your progressive overload recommendations.
Gemini Progressive Overload Analysis
Schedule & Nagging Reminders
Set a due time, then get nagged at your chosen interval until it is marked done. Optionally repeat daily, weekly and more.
Add Task / Homework
Log Workout Exercise
Add Schedule & Reminder Item
Settings & Two-Way Google Sheets Sync
Dark Appearance
Toggle dark mode styling for the app.
Google Sheets Web App URL
Enter your Google Apps Script Web App URL below to enable robust two-way synchronization.
Google Apps Script Setup Instructions
Create a Google Sheet with 3 tabs: Tasks, Workouts, and Schedule. Then open Extensions > Apps Script, paste the code below, and deploy as a Web App (Access: "Anyone").
function doPost(e) {
try {
var data = JSON.parse(e.postData.contents);
var ss = SpreadsheetApp.getActiveSpreadsheet();
// Sync Tasks
if (data.tasks) {
var sheet = ss.getSheetByName("Tasks");
if (!sheet) { sheet = ss.insertSheet("Tasks"); }
sheet.clear();
sheet.getRange("E:E").setNumberFormat("@"); // dueDate stays plain text
sheet.appendRow(["id", "title", "category", "priority", "dueDate", "completed", "deleted", "updatedAt"]);
data.tasks.forEach(function(t) {
sheet.appendRow([t.id, t.title, t.category, t.priority, t.dueDate, t.completed, t.deleted === true, t.updatedAt]);
});
}
// Sync Workouts
if (data.workouts) {
var sheet = ss.getSheetByName("Workouts");
if (!sheet) { sheet = ss.insertSheet("Workouts"); }
sheet.clear();
sheet.getRange("F:F").setNumberFormat("@"); // date stays plain text
sheet.appendRow(["id", "name", "weight", "sets", "reps", "date", "deleted", "updatedAt"]);
data.workouts.forEach(function(w) {
sheet.appendRow([w.id, w.name, w.weight, w.sets, w.reps, w.date, w.deleted === true, w.updatedAt]);
});
}
// Sync Schedule
if (data.schedule) {
var sheet = ss.getSheetByName("Schedule");
if (!sheet) { sheet = ss.insertSheet("Schedule"); }
sheet.clear();
sheet.getRange("C:D").setNumberFormat("@"); // date + time stay plain text
sheet.appendRow(["id", "title", "date", "time", "completed", "nagInterval", "repeat", "deleted", "updatedAt"]);
data.schedule.forEach(function(s) {
sheet.appendRow([s.id, s.title, s.date, s.time, s.completed, s.nagInterval, s.repeat, s.deleted === true, s.updatedAt]);
});
}
return ContentService.createTextOutput(JSON.stringify({status: "success"}))
.setMimeType(ContentService.MimeType.JSON);
} catch (err) {
return ContentService.createTextOutput(JSON.stringify({status: "error", message: err.toString()}))
.setMimeType(ContentService.MimeType.JSON);
}
}
function doGet(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var tz = ss.getSpreadsheetTimeZone();
var result = {};
["Tasks", "Workouts", "Schedule"].forEach(function(tabName) {
var sheet = ss.getSheetByName(tabName);
if (sheet) {
var rows = sheet.getDataRange().getValues();
if (rows.length > 1) {
var headers = rows[0];
var items = [];
for (var i = 1; i < rows.length; i++) {
var obj = {};
for (var j = 0; j < headers.length; j++) {
var v = rows[i][j];
if (v instanceof Date) {
v = Utilities.formatDate(v, tz, headers[j] === "time" ? "HH:mm" : "yyyy-MM-dd");
}
obj[headers[j]] = v;
}
items.push(obj);
}
result[tabName.toLowerCase()] = items;
} else {
result[tabName.toLowerCase()] = [];
}
} else {
result[tabName.toLowerCase()] = [];
}
});
return ContentService.createTextOutput(JSON.stringify(result))
.setMimeType(ContentService.MimeType.JSON);
}