Skip to content

feat(time): work out the time report from my worklogs - #149

Merged
Bakhomious merged 1 commit into
mainfrom
step/time-report
Oct 29, 2026
Merged

Bakhomious merged 1 commit into
mainfrom
step/time-report

Conversation

@Bakhomious

Copy link
Copy Markdown
Contributor

Brief

Goal. The time service's report, worked out from my worklogs: the hours I logged on each of the last 7 days, the weekdays among them with nothing logged, and this week's hours against the 40-hour target. It's a pure package, services/time/internal/report, for the service that the next step brings. The third of time-tracking's twelve steps (decision 181).

Scope.

  • services/time/internal/report/report.go:
    • the types Report (Days, Week), Day (Date, LoggedHours, Missing) and Week (LoggedHours, TargetHours);
    • Span(now): the span of time that the report at now covers;
    • New(worklogs, now): the report at now of the worklogs.
  • docs/plan.md: decision 181 (the split, twelve steps, the report's rules), §0 row, §8.2 the new step marked done and time-service reworded.

Out of scope.

  • The service, its poll and GET /time/report (time-service).
  • Holidays and days off (a §0 idea).
  • Worklogs that start after today.
  • The target as a setting.

Acceptance criteria.

  1. Span(now) returns the start of the day six days before now's, and the start of the day after now's, in now's location. In a zone whose clocks skip midnight, a day starts as they skip it (America/Havana's 01:00), not in the day before.

  2. New lists the 7 days that end with now's, the earliest first. Each has:

    • its date, as 2006-01-02;
    • the hours of the worklogs that start on it in now's location.

    A worklog that crosses midnight counts wholly on the day it starts. A worklog of another day is left out.

  3. A day is missing when it's Monday to Friday, before today, with nothing logged. Today, Saturdays and Sundays never are, and nor is a day with any time logged.

  4. The week's logged hours add up the days from this week's Monday through today; the days before Monday don't count. Its target is 40 hours.

  5. Hours are the time logged in hours, as a number (1 h 30 m is 1.5). Sums add up durations before they turn into hours.

  6. The JSON is {"days":[{"date","loggedHours","missing"}…],"week":{"loggedHours","targetHours"}}, with days always an array of 7.

  7. Nothing reads the clock or the local zone: now brings both.

  8. mise run check passes.

What changed

  • services/time/internal/report/report.go (new), the time service's first package:

    • Report, Day and Week, as in the brief;
    • Span(now) and New(worklogs, now);
    • the helpers dateOf, dayStart, mondayOf and weekday.

    Days are counted on civil dates at midnight UTC, where no clock changes. dayStart uses time.Date's midnight, unless that falls on the day before. In that case it takes the end of the zone then in effect, which is the moment the clocks skip midnight. Durations are added up first and turned into hours once.

  • span_test.go (new): Span in Budapest across both clock changes, at midnight, a nanosecond before midnight, in UTC, and in the four zones whose clocks skip midnight in 2026: Havana, Santiago, Beirut and Cairo.

  • report_test.go (new) covers:

    • each day's sum, with a worklog before the span, the clocks going back, 6 + 12 minutes making exactly 0.3, a worklog that crosses midnight, a +05:30 start, today, and tomorrow left out;
    • the week from each weekday;
    • the missing weekdays;
    • the days in now's zone;
    • the JSON.
  • docs/plan.md:

    • decision 181: the split into twelve steps, the span, each day, the week, hours, no clock and no zone read, and what was rejected;
    • the §0 row;
    • §8.2: time-report marked done, and time-service and time-page reworded to say "time report".

QA

  • Every acceptance criterion has a test. A grep finds no time.Now, time.Local, .Local(), time.Since or time.Until in the package (AC7).

  • Zones: a brute-force probe went through every zone in macOS's tzdata 2025c, every day from 2026 to 2030. It found 40 days that don't start at midnight:

    • in 25 of them, time.Date puts midnight on the day before (Havana, Santiago, Azores, and their aliases), and dayStart's fix gives the day's real start;
    • in the 15 others (Beirut, Cairo, Egypt), time.Date already gives 01:00, the moment the clocks skip.

    None of these days is the first of a month, and every gap is one hour.

  • Mutations: 47 of 47 caught (go test ./services/time/...). They cover the span's bounds and zone, the skipped-midnight fix, the days' zone, the clock, the number of days, the date's layout, hours, sums, each rule for missing days, the week's Monday, the target and every JSON name. The first run left five survivors, and QA added tests for them:

    • Days and today in UTC or in the machine's zone. On this Mac, the local zone is Budapest, as in the sums test, and no worklog started between Budapest's midnight and UTC's. A new test runs in Pacific/Kiritimati (UTC+14), with a worklog that starts at 00:30 there, the day before in UTC. The four survivors are now caught under the Mac's zone and under TZ=UTC, as on CI. The tests pass with the machine in UTC, Havana, Kiritimati, Kolkata and Niue.
    • The week added up as hours. The sums test's week, 4.55, comes out exact in floating point either way, and the week test used whole hours. The week test now logs a tenth of an hour each day. Three tenths added up as hours make 0.30000000000000004.

    One mutant first failed to build (monday unused) and was rewritten. Two mutants were left out because they behave the same on every day from 2026 to 2030: < for != in the fix (no skipped midnight falls on a month's first day), and adding an hour for ZoneBounds (every gap is one hour).

  • Coverage of the package: 100%.

Review

No blocking findings. Non-blocking:

  • Lines of code (without blanks and comments): report.go 73, report_test.go 114, span_test.go 98.
  • If a zone's gap started before midnight ahead of UTC, the day would start late, at the first instant after the gap's end. No zone has such a gap from 2026 to 2030 (the probe), but a later tzdata could add one.
  • weekday returns whether a date is Monday to Friday. Its name is close to time.Weekday, but it reads well where it's used.
  • Holidays count as missing, and the target is fixed at 40 hours (out of scope; a §0 idea).
  • For time-service: the pod's time.Local is UTC unless TZ is set and the image has tzdata. Check both there, and in homelab-time.

Gates

mise run --jobs 1 check passed locally:

  • Go fmt; golangci-lint (0 issues); buf; govulncheck (none);
  • Go tests with -race, Go coverage 95.9% total;
  • Biome, svelte-check, tsc, knip, pnpm audit;
  • Vitest, 1,082 tests;
  • Playwright E2E, 319 passed and 7 skipped.

SonarQube Cloud runs on demand (decision 132).

Merged under the user's standing OK (2026-10-26): merge once CI's check is green, and review everything at the end.

The time service's report package: the hours logged on each of the last 7 days, the weekdays before today with nothing logged, and this week's hours against the 40-hour target. Days follow the zone of the time given, and start as the clocks skip midnight where they do (decision 181).
@Bakhomious
Bakhomious merged commit 6203e47 into main Oct 29, 2026
2 checks passed
@Bakhomious
Bakhomious deleted the step/time-report branch October 29, 2026 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant