Better account-based wait status

This commit is contained in:
alejo committed 2026-10-28 13:11:54 -03:00
1 parent 10badb282c
commit 33567db5a3
14 files changed
+183 -57

No files matched your search

+18 -11
View File
@@ -91,17 +91,22 @@ goes back to `$VISUAL` / `$EDITOR` / nano / vim / vi. The choice is stored in
* Waits belong to the account: `acc wait <name> <time>` (or `tsk restat <task> wait * Waits belong to the account: `acc wait <name> <time>` (or `tsk restat <task> wait
<time>`, which sets its account's wait). Until the time the account's Prompt <time>`, which sets its account's wait). Until the time the account's Prompt
and Testing tasks show `Wait HH:MM`; after it they show `Limit reset`, until and Testing tasks show `Wait HH:MM`; after it they show `Limit reset`, until
you run `acc unwait <name>`, which also clears a stale one. Uploading, Frozen you run `acc unwait <name>`. Uploading, Frozen and Done tasks are not
and Done tasks are not affected, and the tasks keep their own stored status affected, and the tasks keep their own stored status underneath. A wait that
underneath. A wait that no task shows (no tasks, or only Done / Frozen / no task shows (no tasks, or only Done / Frozen / Uploading ones) is listed in
Uploading ones) is listed in the account row as `(wait HH:MM)` or the account row as `(wait)` or `(limit reset)` and keeps the account Busy.
`(limit reset)` and keeps the account Busy. Tasks without an account keep Tasks without an account keep their own wait.
their own wait. * A task you set explicitly to `testing` or `prompt` (`tsk restat <task>
testing`, `tsk add <task> <acc> testing`) shows that status, even while its
account is waiting: that task ignores the wait, and the account's other tasks
keep following it. `tsk restat <task> wait` (no time) makes it follow the
wait again, and a new `acc wait` or `acc unwait` applies to every task again.
Adding a task without a status leaves it following the account.
* Auto unwait: once an account's limit has reset (its tasks show `Limit reset`), * Auto unwait: once an account's limit has reset (its tasks show `Limit reset`),
explicitly setting a task on it to `testing` or `prompt` (`tsk add <task> <acc> setting a task on it explicitly to `testing` or `prompt` clears that finished
testing`, `tsk restat <task> testing`) clears that finished wait, because the wait, because the account is in use again, and its tasks go back to their own
account is in use again. Adding a task without a status does not, and a wait status. A wait that is still running is never cleared automatically (use
that is still running is never cleared automatically (use `acc unwait`). `acc unwait`).
* A time is only optional while the account is already waiting: then * A time is only optional while the account is already waiting: then
`tsk restat <task> wait` (or `tsk add <task> <acc> wait`) just makes the task `tsk restat <task> wait` (or `tsk add <task> <acc> wait`) just makes the task
follow that wait. Otherwise (no account, or its wait is over) a time is follow that wait. Otherwise (no account, or its wait is over) a time is
@@ -143,7 +148,9 @@ against it behaves exactly like typing the commands. Build it with the project
* After a change, `lls_last_messages(db)` holds what the prompt would have * After a change, `lls_last_messages(db)` holds what the prompt would have
printed ("Account 'A' is back in use: ..."). Queries do not reset it. printed ("Account 'A' is back in use: ..."). Queries do not reset it.
* Statuses are the text you would type: `"prompt"`, `"testing"`, `"wait 4:30pm"`, * Statuses are the text you would type: `"prompt"`, `"testing"`, `"wait 4:30pm"`,
`"wait"` (only while the account is waiting)... Waits belong to the account. `"wait"` (only while the account is waiting)... Waits belong to the account;
a task set to `"testing"` / `"prompt"` while it waits ignores the wait
(`overrides_wait` is 1) and `"wait"` makes it follow the account again.
* `lls_tasks` / `lls_accounts` return lists in the order of the panels, with the * `lls_tasks` / `lls_accounts` return lists in the order of the panels, with the
computed status (`status`, `status_label`, `until`) and the stored one underneath computed status (`status`, `status_label`, `until`) and the stored one underneath
(`stored_status`). Strings in a list are valid until `lls_task_list_free`. (`stored_status`). Strings in a list are valid until `lls_task_list_free`.
+2
View File
@@ -46,6 +46,8 @@ public:
void setTaskAccount(const std::string& name, const std::optional<std::string>& account); void setTaskAccount(const std::string& name, const std::optional<std::string>& account);
void setTaskStatus(const std::string& name, const std::string& status); void setTaskStatus(const std::string& name, const std::string& status);
void setTaskNotes(const std::string& name, const std::string& notes); void setTaskNotes(const std::string& name, const std::string& notes);
void setTaskWaitOverride(const std::string& name, bool value);
void clearWaitOverrides(const std::string& account); // every task of the account follows its wait again
private: private:
using Param = std::optional<std::string>; using Param = std::optional<std::string>;
+1
View File
@@ -8,6 +8,7 @@ struct TaskRow {
std::optional<std::string> account; std::optional<std::string> account;
std::string status; // serialized, see StatusUtil::serialize std::string status; // serialized, see StatusUtil::serialize
std::string notes; // free text, any length (stored as SQLite TEXT) std::string notes; // free text, any length (stored as SQLite TEXT)
bool waitOverride = false; // set to Testing / Prompt while its account waits: ignores that wait
}; };
struct AccountRow { struct AccountRow {
+1 -2
View File
@@ -72,9 +72,8 @@ private:
std::string existingAccount(const std::string& name) const; std::string existingAccount(const std::string& name) const;
TaskRow existingTask(const std::string& name) const; TaskRow existingTask(const std::string& name) const;
std::optional<std::time_t> runningWait(const std::string& account) const; std::optional<std::time_t> runningWait(const std::string& account) const;
void resumeAccount(const std::string& account); void taskInUse(const std::string& taskName, const std::string& accountName);
void makeTaskFollowAccount(const TaskRow& task); void makeTaskFollowAccount(const TaskRow& task);
void noteAccountWait(const std::string& taskName);
void note(const std::string& message); void note(const std::string& message);
Database& db_; Database& db_;
+3
View File
@@ -28,6 +28,9 @@ struct TaskInfo {
Status storedStatus; // what is saved underneath Status storedStatus; // what is saved underneath
std::string notes; std::string notes;
std::size_t noteLines = 0; std::size_t noteLines = 0;
// Set to Testing / Prompt while its account waits: it shows its own status
// and ignores the wait, while the account's other tasks keep following it.
bool overridesWait = false;
}; };
struct AccountInfo { struct AccountInfo {
+6 -1
View File
@@ -16,7 +16,11 @@
* or just "wait" (only while the task's account is already waiting). * or just "wait" (only while the task's account is already waiting).
* - A wait belongs to the account: setting a task to "wait <time>" sets its * - A wait belongs to the account: setting a task to "wait <time>" sets its
* account's wait, and the account's Prompt / Testing tasks then show Wait * account's wait, and the account's Prompt / Testing tasks then show Wait
* until that time and Limit reset after it (see the README). * until that time and Limit reset after it. A task you explicitly set to
* "testing" or "prompt" while the wait is running shows that status instead
* (overrides_wait = 1) and the other tasks keep waiting; "wait" makes it follow
* the account again. If the wait is already over (Limit reset), setting a task
* to testing / prompt clears it: the account is in use again (see the README).
* - Times are Unix seconds (UTC); clock-time text like "4:30pm" is read in the * - Times are Unix seconds (UTC); clock-time text like "4:30pm" is read in the
* local time zone, and a time that has passed today means tomorrow. * local time zone, and a time that has passed today means tomorrow.
* *
@@ -88,6 +92,7 @@ typedef struct lls_task_info {
int64_t until; /* for Wait / Limit reset: when the wait ends (ended); else 0 */ int64_t until; /* for Wait / Limit reset: when the wait ends (ended); else 0 */
const char* notes; /* the full text, "" if none (any length) */ const char* notes; /* the full text, "" if none (any length) */
int notes_lines; /* number of lines in the notes */ int notes_lines; /* number of lines in the notes */
int overrides_wait; /* 1 if its account waits but this task ignores it (set to Testing / Prompt) */
} lls_task_info; } lls_task_info;
/* An account with its computed state. */ /* An account with its computed state. */
Binary file not shown.
+3
View File
@@ -98,6 +98,7 @@ class Task:
until: Optional[datetime] # for Wait / Limit reset: when the wait ends (ended) until: Optional[datetime] # for Wait / Limit reset: when the wait ends (ended)
notes: str notes: str
notes_lines: int notes_lines: int
overrides_wait: bool # its account waits, but this task (set to Testing / Prompt) ignores it
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -123,6 +124,7 @@ class _TaskInfo(ctypes.Structure):
("until", ctypes.c_int64), ("until", ctypes.c_int64),
("notes", ctypes.c_char_p), ("notes", ctypes.c_char_p),
("notes_lines", ctypes.c_int), ("notes_lines", ctypes.c_int),
("overrides_wait", ctypes.c_int),
] ]
@@ -400,6 +402,7 @@ class Database:
until=_when(info.until), until=_when(info.until),
notes=_dec(info.notes), notes=_dec(info.notes),
notes_lines=info.notes_lines, notes_lines=info.notes_lines,
overrides_wait=bool(info.overrides_wait),
)) ))
return result return result
finally: finally:
+84 -3
View File
@@ -215,11 +215,92 @@ class WaitTests(Base):
self.assertEqual(self.db.task("a-prompt").status, lls.Status.PROMPT) self.assertEqual(self.db.task("a-prompt").status, lls.Status.PROMPT)
self.assertTrue(any("back in use" in m for m in self.db.messages)) self.assertTrue(any("back in use" in m for m in self.db.messages))
def test_a_running_wait_is_never_cleared_automatically(self): def test_explicit_testing_or_prompt_shows_that_status_while_waiting(self):
self.db.set_account_wait("A", "6pm")
self.db.set_task_status("a-testing", "testing") # explicit
task = self.db.task("a-testing")
self.assertEqual((task.status, task.status_label, task.overrides_wait), (lls.Status.TESTING, "Testing", True))
self.assertTrue(any("ignores it" in m for m in self.db.messages))
self.db.set_task_status("a-prompt", "prompt")
self.assertEqual(self.db.task("a-prompt").status_label, "Prompt")
self.db.add_task("n1", "A", "testing")
self.assertEqual(self.db.task("n1").status, lls.Status.TESTING)
self.db.add_task("n2", "A") # no status given: follows the wait
self.assertEqual(self.db.task("n2").status, lls.Status.WAIT)
self.assertFalse(self.db.task("n2").overrides_wait)
# the account itself is still waiting, nothing was cleared
self.assertEqual(self.db.account("A").wait_state, lls.WaitState.WAITING)
def test_other_tasks_keep_waiting_and_wait_makes_a_task_follow_again(self):
self.db.set_account_wait("A", "6pm")
self.db.add_task("a-other", "A")
self.db.set_task_status("a-testing", "testing")
self.assertEqual(self.db.task("a-other").status, lls.Status.WAIT)
self.assertEqual(self.db.task("a-prompt").status, lls.Status.WAIT)
self.db.set_task_status("a-testing", "wait") # no time: follow the running wait again
task = self.db.task("a-testing")
self.assertEqual((task.status, task.overrides_wait), (lls.Status.WAIT, False))
self.assertIn("now follows account 'A'", self.db.messages[0])
def test_a_new_wait_applies_to_every_task_again(self):
self.db.set_account_wait("A", "6pm") self.db.set_account_wait("A", "6pm")
self.db.set_task_status("a-testing", "testing") self.db.set_task_status("a-testing", "testing")
self.assertEqual(self.db.account("A").wait_state, lls.WaitState.WAITING) self.db.set_account_wait("A", "7pm") # the account hit its limit again
self.assertEqual(self.db.task("a-testing").status, lls.Status.WAIT) task = self.db.task("a-testing")
self.assertEqual((task.status_label, task.overrides_wait), ("Wait 19:00", False))
def test_unwait_clears_the_overrides_too(self):
self.db.set_account_wait("A", "6pm")
self.db.set_task_status("a-testing", "testing")
self.assertTrue(self.db.clear_account_wait("A"))
self.assertFalse(self.db.task("a-testing").overrides_wait)
self.db.set_account_wait("A", "8pm")
self.assertEqual(self.db.task("a-testing").status_label, "Wait 20:00")
def test_an_override_survives_the_wait_ending(self):
self.db.set_account_wait("A", "1pm")
self.at(12, 30)
self.db.set_task_status("a-testing", "testing") # pinned while the wait runs
self.at(14) # the wait is over
self.assertEqual(self.db.task("a-testing").status, lls.Status.TESTING)
self.assertEqual(self.db.task("a-prompt").status, lls.Status.LIMIT_RESET)
self.assertEqual(self.db.account("A").wait_state, lls.WaitState.LIMIT_RESET)
# resuming another task after the reset clears the finished wait for everyone
self.db.set_task_status("a-prompt", "prompt")
self.assertEqual(self.db.account("A").wait_state, lls.WaitState.NONE)
self.assertEqual(self.db.task("a-testing").status, lls.Status.TESTING)
self.assertEqual(self.db.task("a-prompt").status, lls.Status.PROMPT)
def test_the_override_ends_when_the_status_changes_or_the_task_moves(self):
self.db.set_account_wait("A", "6pm")
self.db.set_task_status("a-testing", "testing")
self.db.set_task_status("a-testing", "uploading")
self.assertFalse(self.db.task("a-testing").overrides_wait)
self.db.set_task_status("a-testing", "testing")
self.assertTrue(self.db.task("a-testing").overrides_wait)
self.db.set_task_account("a-testing", "B") # B is not waiting
task = self.db.task("a-testing")
self.assertEqual((task.account, task.status, task.overrides_wait), ("B", lls.Status.TESTING, False))
def test_a_wait_no_task_shows_when_every_task_ignores_it(self):
self.db.set_account_wait("A", "6pm")
self.db.set_task_status("a-testing", "testing")
self.db.set_task_status("a-prompt", "prompt") # a-done is Done
account = self.db.account("A")
self.assertEqual((account.level, account.wait_state, account.wait_listed),
(lls.Level.BUSY, lls.WaitState.WAITING, True))
def test_overrides_are_saved(self):
self.db.set_account_wait("A", "6pm")
self.db.set_task_status("a-testing", "testing")
self.db.save()
with lls.Database(self.path) as other:
other.set_clock(NOON)
self.assertTrue(other.task("a-testing").overrides_wait)
self.assertEqual(other.task("a-prompt").status, lls.Status.WAIT)
def test_account_wait_nothing_shows(self): def test_account_wait_nothing_shows(self):
self.db.add_account("Empty") self.db.add_account("Empty")
+23 -3
View File
@@ -20,7 +20,8 @@ const char* const kSchema =
" id TEXT NOT NULL PRIMARY KEY COLLATE NOCASE," " id TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,"
" account TEXT COLLATE NOCASE REFERENCES Account(id) ON UPDATE CASCADE ON DELETE SET NULL," " account TEXT COLLATE NOCASE REFERENCES Account(id) ON UPDATE CASCADE ON DELETE SET NULL,"
" status TEXT NOT NULL DEFAULT 'prompt'," " status TEXT NOT NULL DEFAULT 'prompt',"
" notes TEXT NOT NULL DEFAULT ''" " notes TEXT NOT NULL DEFAULT '',"
" wait_override INTEGER NOT NULL DEFAULT 0"
");" ");"
"CREATE INDEX IF NOT EXISTS idx_task_account ON Task(account);"; "CREATE INDEX IF NOT EXISTS idx_task_account ON Task(account);";
@@ -108,6 +109,15 @@ void Database::createSchema() {
exec("ALTER TABLE Account ADD COLUMN cooldown TEXT;"); exec("ALTER TABLE Account ADD COLUMN cooldown TEXT;");
} }
// Files from before a task could ignore its account's wait have no such column.
bool hasOverride = false;
for (const Row& row : query("PRAGMA table_info(Task)")) {
hasOverride = hasOverride || (row.size() > 1 && row[1] && *row[1] == "wait_override");
}
if (!hasOverride) {
exec("ALTER TABLE Task ADD COLUMN wait_override INTEGER NOT NULL DEFAULT 0;");
}
// Waits used to be set on each task. They belong to the account now: move // Waits used to be set on each task. They belong to the account now: move
// them over (the latest one wins) and let those tasks follow the account. // them over (the latest one wins) and let those tasks follow the account.
exec( exec(
@@ -288,7 +298,7 @@ std::optional<std::string> Database::findTask(const std::string& name) const {
} }
std::optional<TaskRow> Database::getTask(const std::string& name) const { std::optional<TaskRow> Database::getTask(const std::string& name) const {
std::vector<Row> rows = query("SELECT id, account, status, notes FROM Task WHERE id = ?1", {name}); std::vector<Row> rows = query("SELECT id, account, status, notes, wait_override FROM Task WHERE id = ?1", {name});
if (rows.empty()) { if (rows.empty()) {
return std::nullopt; return std::nullopt;
} }
@@ -297,17 +307,19 @@ std::optional<TaskRow> Database::getTask(const std::string& name) const {
task.account = rows[0][1]; task.account = rows[0][1];
task.status = *rows[0][2]; task.status = *rows[0][2];
task.notes = *rows[0][3]; task.notes = *rows[0][3];
task.waitOverride = rows[0][4] && *rows[0][4] != "0";
return task; return task;
} }
std::vector<TaskRow> Database::listTasks() const { std::vector<TaskRow> Database::listTasks() const {
std::vector<TaskRow> tasks; std::vector<TaskRow> tasks;
for (const Row& row : query("SELECT id, account, status, notes FROM Task ORDER BY id")) { for (const Row& row : query("SELECT id, account, status, notes, wait_override FROM Task ORDER BY id")) {
TaskRow task; TaskRow task;
task.id = *row[0]; task.id = *row[0];
task.account = row[1]; task.account = row[1];
task.status = *row[2]; task.status = *row[2];
task.notes = *row[3]; task.notes = *row[3];
task.waitOverride = row[4] && *row[4] != "0";
tasks.push_back(task); tasks.push_back(task);
} }
return tasks; return tasks;
@@ -334,6 +346,14 @@ void Database::setTaskStatus(const std::string& name, const std::string& status)
run("UPDATE Task SET status = ?2 WHERE id = ?1", {name, status}); run("UPDATE Task SET status = ?2 WHERE id = ?1", {name, status});
} }
void Database::setTaskWaitOverride(const std::string& name, bool value) {
run("UPDATE Task SET wait_override = ?2 WHERE id = ?1", {name, std::string(value ? "1" : "0")});
}
void Database::clearWaitOverrides(const std::string& account) {
run("UPDATE Task SET wait_override = 0 WHERE account = ?1", {account});
}
void Database::setTaskNotes(const std::string& name, const std::string& notes) { void Database::setTaskNotes(const std::string& name, const std::string& notes) {
run("UPDATE Task SET notes = ?2 WHERE id = ?1", {name, notes}); run("UPDATE Task SET notes = ?2 WHERE id = ?1", {name, notes});
} }
+1 -1
View File
@@ -73,7 +73,7 @@ void Repl::registerCommands() {
{ArgKind::Task, ArgKind::None}, &Repl::cmdTskRename); {ArgKind::Task, ArgKind::None}, &Repl::cmdTskRename);
add("tsk reacc", "tsk reacc <name> <new>", "move a task to another account ('-' = none)", 2, 2, add("tsk reacc", "tsk reacc <name> <new>", "move a task to another account ('-' = none)", 2, 2,
{ArgKind::Task, ArgKind::AccountOrDash}, &Repl::cmdTskReacc); {ArgKind::Task, ArgKind::AccountOrDash}, &Repl::cmdTskReacc);
add("tsk restat", "tsk restat <name> <new>", "change a task's status (plain 'wait' works while its account is waiting)", 2, kUnlimited, add("tsk restat", "tsk restat <name> <new>", "change a task's status (testing/prompt ignore the account's wait; plain 'wait' follows it again)", 2, kUnlimited,
{ArgKind::Task, ArgKind::Status}, &Repl::cmdTskRestat); {ArgKind::Task, ArgKind::Status}, &Repl::cmdTskRestat);
add("tsk notes", "tsk notes <name>", "edit a task's notes in your editor (see 'editor')", 1, 1, add("tsk notes", "tsk notes <name>", "edit a task's notes in your editor (see 'editor')", 1, 1,
{ArgKind::Task}, &Repl::cmdTskNotes); {ArgKind::Task}, &Repl::cmdTskNotes);
+38 -35
View File
@@ -78,20 +78,28 @@ std::optional<std::time_t> Tracker::runningWait(const std::string& account) cons
return std::nullopt; return std::nullopt;
} }
// Setting a task to Testing or Prompt after its account's limit has reset means the // A task was explicitly set to Testing or Prompt. If its account's limit has reset
// account is in use again, so the finished wait is cleared (its Limit reset tasks // (a Limit reset), the account is in use again: the finished wait is cleared and
// go back to their own status). A wait that is still running is left alone. // the account's tasks go back to their own status. If the wait is still running,
void Tracker::resumeAccount(const std::string& account) { // only this task ignores it (it shows the status you gave it); the account's other
std::optional<AccountRow> row = db_.getAccount(account); // tasks keep following the wait.
if (!row || !row->cooldown) { void Tracker::taskInUse(const std::string& taskName, const std::string& accountName) {
std::optional<AccountRow> account = db_.getAccount(accountName);
if (!account || !account->cooldown) {
return; return;
} }
const std::optional<std::time_t> until = StatusUtil::timeFromText(*row->cooldown);
const std::optional<std::time_t> until = StatusUtil::timeFromText(*account->cooldown);
if (until && *until > now()) { if (until && *until > now()) {
db_.setTaskWaitOverride(taskName, true);
note("Account '" + account->id + "' is waiting until " + StatusUtil::clockLabel(*until) + "; '" + taskName +
"' ignores it (tsk restat " + taskName + " wait to follow it again).");
return; return;
} }
db_.setAccountCooldown(row->id, std::nullopt);
note("Account '" + row->id + "' is back in use: its finished wait (Limit reset) was cleared."); db_.setAccountCooldown(account->id, std::nullopt);
db_.clearWaitOverrides(account->id);
note("Account '" + account->id + "' is back in use: its finished wait (Limit reset) was cleared.");
} }
// Prompt and Testing tasks follow their account's wait; anything else becomes Prompt. // Prompt and Testing tasks follow their account's wait; anything else becomes Prompt.
@@ -100,19 +108,8 @@ void Tracker::makeTaskFollowAccount(const TaskRow& task) {
if (!current || !StatusUtil::followsAccountWait(current->kind)) { if (!current || !StatusUtil::followsAccountWait(current->kind)) {
db_.setTaskStatus(task.id, StatusUtil::serialize(StatusUtil::make(StatusKind::Prompt))); db_.setTaskStatus(task.id, StatusUtil::serialize(StatusUtil::make(StatusKind::Prompt)));
} }
} if (task.waitOverride) {
db_.setTaskWaitOverride(task.id, false);
// Tells the user when a task's own status is hidden by its account's wait.
void Tracker::noteAccountWait(const std::string& taskName) {
std::optional<TaskRow> task = db_.getTask(taskName);
std::optional<Status> stored = task ? StatusUtil::deserialize(task->status) : std::nullopt;
if (!task || !task->account || !stored || !StatusUtil::followsAccountWait(stored->kind)) {
return;
}
std::optional<AccountRow> account = db_.getAccount(*task->account);
if (account && account->cooldown) {
note("It shows as Wait / Limit reset because account '" + account->id + "' is waiting (acc unwait " +
account->id + " to clear).");
} }
} }
@@ -146,6 +143,7 @@ void Tracker::renameAccount(const std::string& name, const std::string& newName)
void Tracker::setAccountWait(const std::string& name, std::time_t until) { void Tracker::setAccountWait(const std::string& name, std::time_t until) {
const std::string account = existingAccount(name); const std::string account = existingAccount(name);
db_.setAccountCooldown(account, StatusUtil::timeToText(until)); db_.setAccountCooldown(account, StatusUtil::timeToText(until));
db_.clearWaitOverrides(account); // a new limit applies to every task of the account
note("Account '" + account + "' waits until " + StatusUtil::clockLabel(until) + note("Account '" + account + "' waits until " + StatusUtil::clockLabel(until) +
": its Prompt and Testing tasks show Wait, then Limit reset (acc unwait " + account + " to clear)."); ": its Prompt and Testing tasks show Wait, then Limit reset (acc unwait " + account + " to clear).");
} }
@@ -169,6 +167,7 @@ bool Tracker::clearAccountWait(const std::string& name) {
return false; return false;
} }
db_.setAccountCooldown(account->id, std::nullopt); db_.setAccountCooldown(account->id, std::nullopt);
db_.clearWaitOverrides(account->id);
note("Wait cleared on '" + account->id + "'."); note("Wait cleared on '" + account->id + "'.");
return true; return true;
} }
@@ -188,8 +187,9 @@ void Tracker::addTask(const std::string& name, const std::optional<std::string>&
} }
Status status = StatusUtil::make(StatusKind::Prompt); Status status = StatusUtil::make(StatusKind::Prompt);
const bool bareWait = statusText && StatusUtil::isBareWait(*statusText);
if (statusText) { if (statusText) {
if (StatusUtil::isBareWait(*statusText)) { if (bareWait) {
// "wait" without a time: only while the account is already waiting. // "wait" without a time: only while the account is already waiting.
if (!account || !runningWait(*account)) { if (!account || !runningWait(*account)) {
throw TrackerError(ErrorCode::Invalid, throw TrackerError(ErrorCode::Invalid,
@@ -209,11 +209,8 @@ void Tracker::addTask(const std::string& name, const std::optional<std::string>&
note("Task '" + name + "' added."); note("Task '" + name + "' added.");
if (accountWait) { if (accountWait) {
setAccountWait(*account, status.waitUntil); setAccountWait(*account, status.waitUntil);
} else { } else if (statusText && !bareWait && account && StatusUtil::followsAccountWait(status.kind)) {
if (statusText && account && (status.kind == StatusKind::Testing || status.kind == StatusKind::Prompt)) { taskInUse(name, *account); // an explicit Testing / Prompt
resumeAccount(*account);
}
noteAccountWait(name);
} }
} }
@@ -240,6 +237,9 @@ void Tracker::setTaskAccount(const std::string& name, const std::optional<std::s
account = existingAccount(*accountName); account = existingAccount(*accountName);
} }
db_.setTaskAccount(task.id, account); db_.setTaskAccount(task.id, account);
if (task.waitOverride) {
db_.setTaskWaitOverride(task.id, false);
}
note("Task '" + task.id + "' now on account " + (account ? "'" + *account + "'" : std::string("(none)")) + "."); note("Task '" + task.id + "' now on account " + (account ? "'" + *account + "'" : std::string("(none)")) + ".");
} }
@@ -247,16 +247,17 @@ void Tracker::setTaskStatus(const std::string& name, const std::string& statusTe
const TaskRow task = existingTask(name); const TaskRow task = existingTask(name);
if (StatusUtil::isBareWait(statusText)) { if (StatusUtil::isBareWait(statusText)) {
// "wait" without a time: the task follows its account's running wait. // "wait" without a time: the task follows its account's running wait again.
if (!task.account || !runningWait(*task.account)) { const std::optional<std::time_t> until = task.account ? runningWait(*task.account) : std::nullopt;
if (!until) {
throw TrackerError( throw TrackerError(
ErrorCode::Invalid, ErrorCode::Invalid,
task.account ? "Account '" + *task.account + "' is not waiting, so a time is required: wait <time>" task.account ? "Account '" + *task.account + "' is not waiting, so a time is required: wait <time>"
: "Task '" + task.id + "' has no account, so a time is required: wait <time>"); : "Task '" + task.id + "' has no account, so a time is required: wait <time>");
} }
makeTaskFollowAccount(task); makeTaskFollowAccount(task);
note("Task '" + task.id + "' now follows account '" + *task.account + "'."); note("Task '" + task.id + "' now follows account '" + *task.account + "' (waiting until " +
noteAccountWait(task.id); StatusUtil::clockLabel(*until) + ").");
return; return;
} }
@@ -271,11 +272,13 @@ void Tracker::setTaskStatus(const std::string& name, const std::string& statusTe
} }
db_.setTaskStatus(task.id, StatusUtil::serialize(status)); db_.setTaskStatus(task.id, StatusUtil::serialize(status));
if (task.waitOverride) {
db_.setTaskWaitOverride(task.id, false);
}
note("Task '" + task.id + "' set to " + StatusUtil::label(status) + "."); note("Task '" + task.id + "' set to " + StatusUtil::label(status) + ".");
if (task.account && (status.kind == StatusKind::Testing || status.kind == StatusKind::Prompt)) { if (task.account && StatusUtil::followsAccountWait(status.kind)) {
resumeAccount(*task.account); taskInUse(task.id, *task.account); // an explicit Testing / Prompt
} }
noteAccountWait(task.id);
} }
bool Tracker::setTaskNotes(const std::string& name, const std::string& notes) { bool Tracker::setTaskNotes(const std::string& name, const std::string& notes) {
+2 -1
View File
@@ -96,7 +96,8 @@ std::vector<TaskInfo> buildTasks(const Database& db, std::time_t now) {
accountWait = found->second; accountWait = found->second;
} }
} }
info.status = StatusUtil::resolve(info.storedStatus, accountWait, now); info.overridesWait = row.waitOverride && accountWait && StatusUtil::followsAccountWait(info.storedStatus.kind);
info.status = StatusUtil::resolve(info.storedStatus, info.overridesWait ? std::nullopt : accountWait, now);
tasks.push_back(info); tasks.push_back(info);
} }
+1
View File
@@ -326,6 +326,7 @@ int lls_tasks(lls_db* db, lls_task_list** out_list) {
info.until = timed ? static_cast<int64_t>(task.status.waitUntil) : 0; info.until = timed ? static_cast<int64_t>(task.status.waitUntil) : 0;
info.notes = task.notes.c_str(); info.notes = task.notes.c_str();
info.notes_lines = static_cast<int>(task.noteLines); info.notes_lines = static_cast<int>(task.noteLines);
info.overrides_wait = task.overridesWait ? 1 : 0;
} }
*out_list = list.release(); *out_list = list.release();
}, true); }, true);