Prefix the project name or note column with ~ to mark an entry as
count-but-don't-export. Marked entries are included in summary and
status totals but omitted from all csv output (both --raw and
aggregated, single-day and weekly).
| 09:00 | 17:00 | 8:00 | ~Leave | | Day off |
| 09:00 | 17:00 | 8:00 | Leave | | ~Day off |
The ~ is stripped from whichever field carries it before any
downstream processing, so project map resolution is unaffected.
Implementation:
- parse_table sets skip_csv=True on marked rows and strips the ~
- new filter_skip_csv() helper in parser.py
- to_csv_entries() skips skip_csv rows
- _cmd_csv calls filter_skip_csv() before aggregate_rows()
Parallel work is logged as overlapping entries. resolve_overlaps()
splits the shared time equally using the midpoint of the overlap region:
- Partial overlap: the midpoint becomes the boundary between the two
entries (earlier entry trimmed, later entry delayed).
- Full containment: the containing entry is split into two pieces
surrounding the contained one, with the midpoint rule applied to
the overlap region.
Open entries (no end time) are passed through unchanged.
resolve_overlaps() is called automatically in filter_rows_by_date,
filter_week_sections, and the --input single-day path in cli.py, so
all subcommands benefit without further changes.
- Add status.py with compute_day_status() and compute_week_status()
- Add projected_hours_for_day(): for each entry, use its duration if
closed, or the next entry's start time as the close time if open
- Open entries (start present, end absent) are preserved by the parser
instead of being skipped; aggregate_rows() skips them for summaries
- Expected end is computed by filling remaining hours into available
time slots from the open entry's start; clamped to latest_end if a
pre-logged entry ends later, with a note explaining why
- Projected week total sums projected_hours_for_day() across all days
- Add status subcommand to cli.py with shared source/day arguments
- Add [work] daily_hours / weekly_hours config keys (default 8 / 40)
- Add timesheets.example.toml [work] section
- Add tests for projected_hours_for_day, compute_day_status,
compute_week_status and all DayStatus/WeekStatus fields
Blank (whitespace-only) lines inside a table no longer split it into
separate blocks. They are buffered and discarded if more table rows
follow, enabling patterns like pre-filling a recurring meeting entry
with a blank line separating it from the rest of the day's entries.
- Add joplin.py with fetch_week_note() that walks Work > Timesheets > YYYY
and returns the body of the matching YYYY-WNN note via joppy ClientApi
- Add filter_rows_by_date() to parser.py to extract only rows belonging
to a specific day based on '# ... YYYY-MM-DD' headings in the document
- Update cli.py: input and --joplin are now a mutually exclusive required
group; add --token flag with JOPLIN_TOKEN env var fallback; --date is
parsed into a real date object used for both output and day filtering
- Add joppy as a runtime dependency (lazy-imported in cli.py)
- Add tests for filter_rows_by_date and full mocked coverage of joplin.py
- Update AGENTS.md with Joplin usage, notebook structure, and test rules
The actual Joplin structure has notes directly inside the year notebook
(Work > Timesheets > YYYY), not in per-week sub-notebooks as initially
assumed. fetch_week_note() reflects this flat structure.
- Add extract_table_blocks() to split a document into contiguous table
blocks, ignoring prose, headings, and blank lines between them
- Add parse_document() as the new top-level entry point that runs
extract_table_blocks + detect_has_duration_column + parse_table per
block and returns a combined flat list of rows
- Guard against empty End cells (e.g. in-progress rows) by validating
the end field before calculating duration
- Update cli.py to use parse_document() instead of the manual
detect + parse combo
- Add tests for extract_table_blocks and parse_document, including two
smoke tests against the real 2026-W21 weekly timesheet file