Android: Timesheet - Stop working #289
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
If currently the timesheet is running (within a card), show that in the top-menu, and add a permanent notification with a button to end it.
For this, timesheet needs a "default append file" like reminder has, the cli a new "streamd timesheet break" command, which is then wired up with that button.
Plan
A timesheet command gets the file command its breaks are written into. The CLI gets
streamd <key> break [TEXT...]. When today has an open card, the app says so on the timesheet screen and on the Stream tab, and shows an ongoing notification with a Stop working action that writes the break. The notification side builds on #288. It reuses theStreamdapplication object, thenotify/package and the checks it runs whenever the stream is anotherReady. #288 is implemented first.Decisions (clarified with the user)
append_to, the file command a line is appended to. It is the same field on a reminder:into_fileof a reminder is renamed toappend_to. This is a breaking change. An oldinto_fileis rejected with a pointed message (below).StreamdTheme.colors).cardsubcommand and no start button.breaktakes an optional text, which goes after the tags as withappend. The button of the notification and the app writes none.1. Core:
append_toon reminders and timesheetsconfig/project/file.rs,validate.rs,schema.rs,change.rs,complete.rs,rename.rs,init.toml:into_file→append_toeverywhere: the file model,ReminderCommand.append_to, schema docs, the completion of values (the file commands),change.rs, andrename_command, which renames theappend_toof reminders and of timesheets.into_filein a reminder table is an error at its path:commands.<key>.into_file: `into_file` was renamed; write `append_to = "<value>"`. This is a special case before the generic unknown-field message.CoreError::NoIntoFile→NoAppendTo { key }. The message becomes`<key>` cannot be marked as done: it has no `append_to`, the file command to write into; add one to [commands.<key>].TimesheetCommand.append_to: Option<String>, which is optional, plus its schema entry, itschange.rsline and its completion.append_to:closestsuggestion the reminder has.breakvalue into a file of that command (break_tags, below). Otherwise:commands.<key>.append_to: no tags place { <dimension> = "<BREAK>" }, sobreakcannot write it into a file of; add a tag that does, or removeappend_to``.init.toml: the reminder examples useappend_to.[commands.timesheet]getsappend_to = "daily".2. Core: working and the break
New
commands/timesheet/clock.rs:working<S: Borrow<LocalizedShard>>(command, shards, now, tz) -> Option<Working>, whereWorking { since: DateTime<Utc>, opened: EntrySource }. It returns the open timecard of today (local day intz), i.e. the lastTimecardSourceof today whoseclosedisNone, taken fromextract_timesheets. It returnsNoneif:now(logged ahead), orAn open card of an earlier day is a
DayProblem, not running work.break_tags(command, file, config) -> Option<Vec<String>>returns the tags of a line that placesdimension = values[break]. It usesstate::derive::derive(config, dimension, &inherited_markers, break_value)from the markers that a file offilehands down (theinheritedofreminder/done.rs, moved tofile/so both share it). The result is canonical names, never an alias. With the starter it is@Timesheet @Break.record_break(store, commands, config, shards, key, text: Option<&str>, now, tz) -> Result<Appended, CoreError>joins the tags with the text and appends them throughfile::append(which adds the moment markers). Its errors:CoreError::NoAppendTo(worded for a timesheet:`<key>` cannot record a break: it has no `append_to` …; the variant gets akind, or a second variantNoBreakTarget, whichever reads cleaner);CoreError::NotWorking { key }whenworkingisNone(`<key>`: not working, no card of today is open), so a stray break is never written.Tests on a
MemoryStore:now, open card yesterday, invalid today;record_break: with and without text, created daily file vs. existing one, not working, noappend_to;into_filehint;rename_commandrenaming a file command updatesappend_toof both types.api.rs:timesheet_working(project, stream, key, now) -> Result<Option<Working>, CoreError>,can_break(project, key) -> bool,timesheet_break(store, project, stream, key, text, now) -> Result<Appended, CoreError>.3. CLI
crates/streamd-cli/src/commands/timesheet/:streamd <key> break [TEXT...]is a subcommand built likeappendof R31. It stands alone, so combining it with-i,-dor--debugis a usage error (exit 2). It printsBreak at HH:MM in <path>(paths as in R21) and opens no editor.NotWorkingandNoAppendToprint the message and exit 1.append_to.tests/cli.rs: a break after a card, with text, the not-working error, the missingappend_toerror, the usage errors, andinto_filebeing rejected.4. Bindings
crates/streamd-ffi:Project.timesheetWorking(stream, key, now) -> Working?(sinceas a timestamp,openedas anEntrySource),canBreak(key), andtimesheetBreak(store, stream, key, text?, now) -> Appended.StreamdException:NoIntoFile→NoAppendTo, plusNotWorking.outline.rs: reminder settingsintoFile→appendTo, timesheet settings getappendTo, with the round-trip test.5. Android
core/:StreamSession:working(now): List<Working>gives one entry per timesheet command with an open card:key,since(Instant), theEntrythat opened it, andcanBreak.takeBreak(key, now): TookBreak(Done(id)/Failed(reason)) callstimesheetBreakand thenchanged(file).StreamReading/ a newWorkinginterface onStreamViewModel. AfterDone, the view model commits throughrepository.written(id), asmarkDonedoes.reason()covers the new variants.intoFile→appendTothroughoutStreamCard.kt,ConfigCommandForms.kt(the reminder field is now "Append to"),ConfigDraft.kt,ConfigList.kt(fileCommandsserves both types) and their tests. The timesheet form gets the same "Append to" choice offileCommands.ui/:ScreenBarofTimesheetScreen, or as itsdetail, a working row shows "Working since 09:12 · 2:31" (clockText/minutesTextofTimesheetWeek.kt) with a Stop button (only ifcanBreak). It usessecondaryContainer/onSecondaryContainer, never the warning colour.TimesheetContentgets theWorking?and anonStop.WorkingBannerinComponents.kt, built likeReminderBannerbut tonal. It reads "Working since 09:12 · " with Stop, and tapping it opens the timesheet command (Tab.Commandroute). There is one per working timesheet command, above the reminder banners.StreamContentgets the list.nowthe screen is given. ALaunchedEffectticks it once a minute in the screen, never in the content composable, so screenshots are fixed.strings.xml.notify/(on top of #288):working("Working time", low importance, silent).WorkingNotifications: one ongoing notification (setOngoing(true),setOnlyAlertOnce) per working timesheet command, with id and tag from itskey:setWhen(since)andsetUsesChronometer(true), so Android counts the time without updates.MainActivityon that timesheet command.canBreak.Streamdruns on every newReady(#288), and inReminderWorkerafter its pull. A card opened on the desktop therefore shows up at the next background check. While the app runs, a card written in the app shows at once.PendingIntenttoBreakReceiver, which enqueues an expeditedBreakWorkerwith thekey. The worker:streamd.ready()andsession.takeBreak(key, now).Done(id):repository.written(id), which commits and pushes as the settings say, then withdraws the notification.Failed(reason): replaces the notification with the reason and keeps Stop working. If the reason isNotWorking, it just withdraws it.POST_NOTIFICATIONSrequest of #288 is asked as well when a timesheet command withappend_toexists, not only for a reminder command.Tests:
Unit tests for the pure parts: what to post, update and cancel from the
workinglist and the posted keys, kept pure inWorkingAlerts.ktlikeReminderAlerts; the elapsed text; theappendTorename inConfigListTestandStreamSessionTest, plustakeBreakandworkingthere.New screenshot scenes:
TimesheetWorkingScreenshot: the working row, with and without Stop.StreamWorkingBannerScreenshot: a working banner above a reminder banner.Record them and look at every changed image.
By hand on the emulator: write a card in the app, then the notification appears and counts. Stop writes
@Timesheet @Break @HHMMSSinto today's daily file, commits and pushes, and the banner and the notification go away. A card from the desktop shows after a pull.6. Documentation
REQUIREMENTS.md:append_toand its validation.working, the break tags,streamd <key> break [TEXT...], its output and errors.into_file→append_to, with the rejection message of the old name.appendis also written by a break.README.md:append_to,streamd timesheet break, and the working notification of the app. Mention the rename for existing configurations.AGENTS.md:clock.rs(working,break_tags,record_break) and the sharedinherited.timesheet_working,can_breakandtimesheet_breakin the facade list.into_file→append_torename wherever it is named (reminder module,rename_command, ffioutline.rs, AndroidConfigList).WorkingNotifications/BreakWorkerinnotify/, andWorkingBannerinComponents.kt.Commits (suggested)
feat(reminder)!: into_file is now append_tofeat(timesheet): append_to and the break of a running cardfeat(timesheet): streamd <key> breakfeat(ffi): working, canBreak and timesheetBreakfeat(android)!: append_to in the forms of reminders and timesheetsfeat(android): show running work and stop it from a notificationtest(android): record the working banner and rowdocs: the break of a timesheet and append_toImplemented in PR #291 (branch
289_timesheet-stop-working, 8 commits, following the plan's split).Effort: about 1 hour of agent time (15:21 → about 16:25 on 2026-10-17). Roughly 650k tokens of context processed over the session; this is an estimate, since the harness doesn't report exact usage.
Findings during implementation
check_reachablealready requires that a tag places every timesheet value. So the new break check only fires where the value is placed together with another dimension, becausederivekeeps the other dimensions unchanged. The test uses that case (@Stopplacingtimesheetandproject).derive(config, dim, &[], value)from an empty line finds@Timesheet @Breakdirectly: the search tries combinations of added tags. A placement already handed down by the file type needs no tag, as with reminders.working()takes the moment of the card from the shard that matches theopenedsource. A card logged ahead of now is closed to a zero-length timecard byextract_timesheets, so it is filtered out explicitly.nowlives inApp(not screenshot-tested) and is passed down. This keepsStreamScreen/TimesheetContentdeterministic in layoutlib, which runs no effects.Ready.breaking(timesheet keys thatcanBreak) is computed in the session, sowantsNotificationscan ask for the permission without the UI calling the bindings.setUsesChronometer), so a swiped one comes back. While a card is open, a check is scheduled at the next local midnight to withdraw it.nix flake checkandnix build .#streamd-apppass.