A Small Red Bar Above the Task List
Notes from building a row that answers ‘what should I do right now?’ — a minute-tick timer, a desktop port, and two same-shaped bugs along the way.
A Small Red Bar Above the Task List
Today I added a small bar at the top of the task list. Labeled “Active Now”, in the same red as the calendar’s current-time indicator. Underneath it sit the tasks whose start time has passed but whose end time has not.
The feature itself fits in one sentence. The decisions behind it didn’t.
A Sort Option vs a Separate Section
Two ideas came up at the start. Add a sort option (ACTIVE_NOW) so active tasks float to the top, or carve out a separate section entirely.
A sort option is clean. One line of SQL, fits naturally with the existing sort UI. But hiding the answer to “what should I do right now?” inside a sort menu is weak. If the user doesn’t change the sort, they don’t see it.
A separate section is visually stronger. It has a header, a color, always at the top. More to build, of course.
I went with the latter. Fecit’s Tasks tab leans toward backlog, so injecting a time-based emphasis there blurs the separation a little. But the value of “what’s due right now should surface first” outweighed that blur.
One Minute-Tick Timer Is Enough
Whether a task is active changes with time. A task that ends in five minutes is no longer active in five minutes. Something has to re-evaluate this once a minute.
I almost dropped another setInterval(60_000) into the Tasks tab. But DailyTimelineView (the calendar’s red current-time line) was already running its own. Two of the same thing in two places.
I consolidated to a single global atom. nowMinuteAtom + useNowMinuteTicker. Mounted once at the root layout, aligned to the next minute boundary, then ticking every minute. Both the Tasks tab and the calendar subscribe. No re-fetching from the server — just redrawing the boundary on already-loaded data.
SQL Once, JS Every Minute
If active tasks aren’t on page 1, the section is meaningless. So sorting happens server-side, in SQL.
ORDER BY (start_date <= ? AND end_date >= ? AND state IN (1,2)) DESC,
created_at DESC
Compute “is active” against the query-time “now” and bubble active tasks up. Plays well with pagination.
JS partitions the result a second time. When the minute tick fires, the useMemo recomputes; tasks whose end time just passed slip out of the active section naturally. SQL is the snapshot at query time; JS keeps it honest as time moves.
The hideDatedTasks Conflict
Fecit has a “Hide Dated Tasks” preference. Some users turn it on to keep the backlog free of scheduled noise. But every candidate for the active section is, by definition, dated. With the toggle on, the active section would always be empty — a contradiction.
Going back to user intent resolves it. “Hide dated tasks” means don’t let scheduled items clutter my backlog, not hide what’s happening right now. So I made active tasks an exception to the hideDatedTasks filter in SQL.
WHERE ... AND (
(start_date IS NULL OR end_date IS NULL) -- original meaning
OR (active task) -- exception
)
Giving Users a Choice
After all of that, one decision was still open. Should this section be forced on every user?
Some people deliberately keep their backlog detached from time. Auto-answering “what should I do right now?” can be an interruption rather than help.
I added a new preference field, showActiveNow. Default true (on). A toggle landed in mobile and desktop settings; turning it off disables the SQL ordering, the hideDatedTasks exception, and the JS partitioning all at once. The server keys off a show_active_now flag the client sends.
Default-on because I think this should be the default experience. But it’s optional.
Desktop Took a Different Route
Mobile queries a local SQLite cache, so swapping the ORDER BY was enough. Desktop is different — every load goes to the server, and pagination happens there.
MongoDB’s find().sort() doesn’t sort by computed expressions cleanly. An aggregation pipeline could, but the existing code is all find()-based and the rewrite would be invasive.
I picked a simpler path. Two fetches.
only_active=true: just the active tasks, no paginationexclude_active=true: regular pagination, active excluded
The desktop client takes both, draws the active section on top and the regular list below. Server-side, this was just two extra OR clauses in the query builder.
One thing worth flagging — the server is not fetched every minute. Fetches happen only on page mount, filter change, manual refresh, or SSE events. The minute tick re-partitions an already-loaded array in JS. Polling once a minute is expensive and barely useful.
Two Same-Shaped Bugs Found Along the Way
Mid-feature, two reports came in with the same flavor.
“The label detail screen shows nothing.” “Project state won’t change.”
Different domains, similar symptom. Following the code, it was exactly the same pattern.
The list screens had been refactored for filter/pagination and moved to local state, which broke the writes to the global atom. Detail screens still read from the atom, so an empty atom meant nothing to display. There was a fallback getProject(id) fetch, but it merged via updateItem (update-only by ID), so an ID that wasn’t already in the atom never made it in.
The fix was a one-liner in spirit — also write fetched data into the atom via ID-based additive merge. Same shape applied to Labels and Projects.
What’s interesting is that I’d patched this pattern yesterday (Labels) and still discovered it again today (Projects). One sighting doesn’t immunize you against the same shape in another domain. Going on a checklist for future refactors of this kind.
A Toast Less, an Instant Response
Two side tracks, briefly.
When you create a task with a date set, users with hideDatedTasks on see it vanish from the list. So a “Saved to calendar” toast appears, telling them where it went. But if you’re creating from the calendar screen, you’re already in calendar context. The toast is noise. One line: skip the toast when selectedDate is passed in (which only happens via the calendar entry point).
I also touched up label attach/detach. They had been awaiting the API response before updating local state. The UI froze for the duration of the round-trip. Switched to optimistic — update local first, push a LabelLinkModel with a temporary id immediately, replace it with the real link when the response arrives, roll back to a snapshot on failure. Small change, noticeably different feel.
Same Color as the Calendar’s Line
One last design call.
I started with PRIMARY500 (brand blue) for the section header. Too polite. Not loud enough. A neutral one shade brighter didn’t carry the meaning of “now” well either.
I remembered the calendar’s “current time” indicator is DANGER500 (red), and matched it. With the same color carrying “now” across screens, a red bar elsewhere reads as a now signal. It’s loud, but loud is right here.
Star icon on the left, “Active Now” label next to it, a collapse chevron on the right. Same skeleton as DateSeparator. Different flow, same shape — extending the same principle.
Stitched together, today was one main feature (the active section), two same-shaped bugs, and three small polishes. It still surprises me — and feels familiar — how many decisions go into a small red bar.
“What should I do right now?” is something we ask ourselves often. If a tool can answer in half a second, that’s not a small difference.