Skip to main content

max / goingson

Take quasi 0.39, and say when a running timer started Forward fix for quasi's time-derived node kinds (`ba4dcfae`), and the site that was waiting on the ruling behind them by name. The task row's time cell held a "Timer running" badge and a comment saying the elapsed value was undecided pending quasicoherent `f00244a6`; that ruling landed 2026-08-18 and says the row carries the instant and the renderer does the subtraction on its own clock. So a running timer is `Node::since` of the session's start now, and the badge is gone. The tracked-against-estimated badge is unchanged for every row whose timer is not running.
Author: Max Johnson <me@maxj.phd> · 2026-08-20 15:44 UTC
Signed with PGP, not checked
Commit: 72be59322ad604f383f73cf59aca0309019f42a3
Parent: 98e7deb
4 files changed, +74 insertions, -36 deletions
M Cargo.lock +24 -24
@@ -1387,7 +1387,7 @@
1387 1387 "libc",
1388 1388 "option-ext",
1389 1389 "redox_users",
1390 - "windows-sys 0.60.2",
1390 + "windows-sys 0.61.2",
1391 1391 ]
1392 1392
1393 1393 [[package]]
@@ -1628,7 +1628,7 @@
1628 1628 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
1629 1629 dependencies = [
1630 1630 "libc",
1631 - "windows-sys 0.60.2",
1631 + "windows-sys 0.61.2",
1632 1632 ]
1633 1633
1634 1634 [[package]]
@@ -3629,7 +3629,7 @@
3629 3629 "png 0.18.1",
3630 3630 "serde",
3631 3631 "thiserror 2.0.20",
3632 - "windows-sys 0.60.2",
3632 + "windows-sys 0.61.2",
3633 3633 ]
3634 3634
3635 3635 [[package]]
@@ -3762,7 +3762,7 @@
3762 3762 source = "registry+https://github.com/rust-lang/crates.io-index"
3763 3763 checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
3764 3764 dependencies = [
3765 - "windows-sys 0.60.2",
3765 + "windows-sys 0.61.2",
3766 3766 ]
3767 3767
3768 3768 [[package]]
@@ -4696,7 +4696,7 @@
4696 4696
4697 4697 [[package]]
4698 4698 name = "quasi-http"
4699 - version = "0.38.0"
4699 + version = "0.39.0"
4700 4700 dependencies = [
4701 4701 "form_urlencoded",
4702 4702 "http",
@@ -4705,7 +4705,7 @@
4705 4705
4706 4706 [[package]]
4707 4707 name = "quasi-notifs"
4708 - version = "0.38.0"
4708 + version = "0.39.0"
4709 4709 dependencies = [
4710 4710 "quasi-router",
4711 4711 "synckit-config",
@@ -4713,14 +4713,14 @@
4713 4713
4714 4714 [[package]]
4715 4715 name = "quasi-router"
4716 - version = "0.38.0"
4716 + version = "0.39.0"
4717 4717 dependencies = [
4718 4718 "makeover-layout",
4719 4719 ]
4720 4720
4721 4721 [[package]]
4722 4722 name = "quasi-tauri"
4723 - version = "0.38.0"
4723 + version = "0.39.0"
4724 4724 dependencies = [
4725 4725 "http",
4726 4726 "quasi-http",
@@ -4746,7 +4746,7 @@
4746 4746
4747 4747 [[package]]
4748 4748 name = "quasi-webview"
4749 - version = "0.38.0"
4749 + version = "0.39.0"
4750 4750 dependencies = [
4751 4751 "docengine",
4752 4752 "makeover-layout",
@@ -5185,7 +5185,7 @@
5185 5185 "errno",
5186 5186 "libc",
5187 5187 "linux-raw-sys",
5188 - "windows-sys 0.60.2",
5188 + "windows-sys 0.61.2",
5189 5189 ]
5190 5190
5191 5191 [[package]]
@@ -5242,7 +5242,7 @@
5242 5242 "security-framework",
5243 5243 "security-framework-sys",
5244 5244 "webpki-root-certs",
5245 - "windows-sys 0.60.2",
5245 + "windows-sys 0.61.2",
5246 5246 ]
5247 5247
5248 5248 [[package]]
@@ -5782,7 +5782,7 @@
5782 5782 checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
5783 5783 dependencies = [
5784 5784 "libc",
5785 - "windows-sys 0.60.2",
5785 + "windows-sys 0.61.2",
5786 5786 ]
5787 5787
5788 5788 [[package]]
@@ -6557,7 +6557,7 @@
6557 6557 "getrandom 0.4.3",
6558 6558 "once_cell",
6559 6559 "rustix",
6560 - "windows-sys 0.60.2",
6560 + "windows-sys 0.61.2",
6561 6561 ]
6562 6562
6563 6563 [[package]]
@@ -6579,7 +6579,7 @@
6579 6579 "parking_lot",
6580 6580 "rustix",
6581 6581 "signal-hook",
6582 - "windows-sys 0.60.2",
6582 + "windows-sys 0.61.2",
6583 6583 ]
6584 6584
6585 6585 [[package]]
@@ -7061,7 +7061,7 @@
7061 7061 "png 0.18.1",
7062 7062 "serde",
7063 7063 "thiserror 2.0.20",
7064 - "windows-sys 0.60.2",
7064 + "windows-sys 0.61.2",
7065 7065 ]
7066 7066
7067 7067 [[package]]
@@ -7096,7 +7096,7 @@
7096 7096 dependencies = [
7097 7097 "memoffset",
7098 7098 "tempfile",
7099 - "windows-sys 0.60.2",
7099 + "windows-sys 0.61.2",
7100 7100 ]
7101 7101
7102 7102 [[package]]
@@ -7626,7 +7626,7 @@
7626 7626 source = "registry+https://github.com/rust-lang/crates.io-index"
7627 7627 checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
7628 7628 dependencies = [
7629 - "windows-sys 0.60.2",
7629 + "windows-sys 0.61.2",
7630 7630 ]
7631 7631
7632 7632 [[package]]
@@ -8496,25 +8496,25 @@
8496 8496 "winnow 1.0.4",
8497 8497 ]
8498 8498
8499 - [[patch.unused]]
8500 - name = "makeover-immediate"
8501 - version = "0.30.0"
8502 -
8503 8499 [[patch.unused]]
8504 8500 name = "ops-status"
8505 8501 version = "0.1.0"
8506 8502
8503 + [[patch.unused]]
8504 + name = "makeover-immediate"
8505 + version = "0.30.0"
8506 +
8507 8507 [[patch.unused]]
8508 8508 name = "quasi-axum"
8509 - version = "0.38.0"
8509 + version = "0.39.0"
8510 8510
8511 8511 [[patch.unused]]
8512 8512 name = "quasi-basics"
8513 - version = "0.38.0"
8513 + version = "0.39.0"
8514 8514
8515 8515 [[patch.unused]]
8516 8516 name = "quasi-immediate"
8517 - version = "0.38.0"
8517 + version = "0.39.0"
8518 8518
8519 8519 [[patch.unused]]
8520 8520 name = "quasi-store"
@@ -38,7 +38,7 @@
38 38 # `quasi` feature. Optional here as well as below so a default build compiles
39 39 # neither: one `dep:` name covers both tables, which is what lets the feature
40 40 # gate the build script and the crate together.
41 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.38", optional = true }
41 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.39", optional = true }
42 42
43 43 [dependencies]
44 44 goingson-core = { workspace = true }
@@ -56,10 +56,10 @@
56 56 # dependency's source even with the feature off -- so a public clone could not
57 57 # build at all against a URL it could not reach. quasi went public that day and
58 58 # the URL moved here with it.
59 - quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.38", optional = true }
60 - quasi-http = { git = "https://makenot.work/git/max/quasi.git", version = "0.38", optional = true }
61 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.38", optional = true }
62 - quasi-tauri = { git = "https://makenot.work/git/max/quasi.git", version = "0.38", optional = true }
59 + quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.39", optional = true }
60 + quasi-http = { git = "https://makenot.work/git/max/quasi.git", version = "0.39", optional = true }
61 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.39", optional = true }
62 + quasi-tauri = { git = "https://makenot.work/git/max/quasi.git", version = "0.39", optional = true }
63 63
64 64 # The notification declaration, and it is NOT behind the `quasi` feature: the
65 65 # watcher is desktop app behaviour rather than a described screen, and the
@@ -76,7 +76,7 @@
76 76 # before it has ported its screens cannot take it. `notifications::Notifier` is
77 77 # the eight lines meanwhile, and it goes when the flip makes quasi-router
78 78 # unconditional here.
79 - quasi-notifs = { git = "https://makenot.work/git/max/quasi.git", version = "0.38", features = ["synckit"] }
79 + quasi-notifs = { git = "https://makenot.work/git/max/quasi.git", version = "0.39", features = ["synckit"] }
80 80
81 81 # Tauri
82 82 tauri = { workspace = true, features = ["image-png", "devtools"] }
@@ -147,6 +147,8 @@
147 147 // choice made here. Same allow, for the same reason, as quasi-axum's tests.
148 148 #![allow(clippy::needless_pass_by_value)]
149 149
150 + use std::time::{Duration, SystemTime};
151 +
150 152 use goingson_core::{
151 153 MilestoneId, Priority, ProjectId, SortDirection, Task, TaskFilterQuery, TaskId, TaskSortColumn,
152 154 TaskStatus,
@@ -552,14 +554,16 @@
552 554
553 555 /// What the row says about time spent on the task.
554 556 ///
555 - /// The tracked-against-estimated badge, and whether a timer is running. What it
556 - /// does not say is *how long* the running timer has been running: the shipped
557 - /// row draws a live elapsed value ticked by one app-wide interval, and who owns
558 - /// a ticking clock is quasicoherent `f00244a6`, undecided. A description that
559 - /// said "18m" here would be describing the moment it was rendered.
557 + /// The tracked-against-estimated badge, for a task whose timer is not running.
558 + /// A running one says how long it has been running instead, and that is
559 + /// [`running_for`] rather than a badge: quasicoherent `f00244a6` decided
560 + /// 2026-08-18 that a readout derived from the current time is a node carrying an
561 + /// instant, so the row says when the timer started and the renderer says how
562 + /// long ago that was. A badge saying "18m" would be describing the moment it was
563 + /// rendered.
560 564 fn time_token(task: &Task) -> Option<Tag> {
561 565 if task.has_active_timer() {
562 - return Some(Tag::badge("Timer running").tone(Tone::Info));
566 + return None;
563 567 }
564 568 let tracked = task.actual_minutes;
565 569 match (tracked, task.estimated_minutes) {
@@ -577,6 +581,17 @@
577 581 }
578 582 }
579 583
584 + /// When a running timer started, for the readout that counts up from it.
585 + ///
586 + /// `None` when nothing is running, which is every row that is not the one being
587 + /// worked on. This is the whole of the app's half: the words, the format and the
588 + /// tick are the renderer's, which is what let `time-tracking.js`'s per-second
589 + /// subtraction stop being the app's problem.
590 + fn running_for(task: &Task) -> Option<SystemTime> {
591 + let started = task.active_session.as_ref()?.started_at;
592 + Some(SystemTime::UNIX_EPOCH + Duration::from_secs(u64::try_from(started.timestamp()).ok()?))
593 + }
594 +
580 595 /// The description cell: what the task is, and everything true of it that has
581 596 /// no column of its own.
582 597 ///
@@ -613,6 +628,10 @@
613 628 });
614 629 }
615 630
631 + if let Some(started) = running_for(task) {
632 + cell = cell.part(Node::since(started));
633 + }
634 +
616 635 if let Some(time) = time_token(task) {
617 636 cell = cell.token(time);
618 637 }
@@ -815,3 +815,22 @@
815 815 assert!(narrowed.contains("No tasks match"), "{narrowed}");
816 816 assert!(narrowed.contains("Clear filters"), "{narrowed}");
817 817 }
818 +
819 + #[tokio::test]
820 + async fn a_running_timer_says_when_it_started_rather_than_how_long_it_has_run() {
821 + // quasicoherent `f00244a6`: the row carries the instant and which way the
822 + // readout runs, and the renderer does the subtraction on its own clock.
823 + // Before the ruling this cell held a "Timer running" badge, because a
824 + // description saying "18m" would have been describing the moment it was
825 + // built.
826 + let state = state().await;
827 + let id = task(&state, "Write the thing");
828 + state
829 + .tasks
830 + .start_timer(id, DESKTOP_USER_ID)
831 + .expect("the timer starts");
832 +
833 + let html = screen(&state);
834 + assert!(html.contains("data-clock=\"since\""), "{html}");
835 + assert!(!html.contains("Timer running"), "{html}");
836 + }