Summarize weekly Primavera timesheet HTML reports into monthly HTML or CSV summaries
  • Go 93.4%
  • JavaScript 2.9%
  • CSS 2.7%
  • HTML 1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-11-02 19:47:08 +01:00
.forgejo/workflows chore: disable CI workflow until runners exist 2026-10-03 12:23:20 +00:00
docs/decisions feat(server): add Material You upload UI 2026-10-06 15:18:05 +00:00
internal Merge pull request 'feat(server): web UI to upload Primavera timesheets and download processed ZIP' (#12) from feat/issue-11-web-ui-serve into main 2026-11-02 19:47:08 +01:00
.gitignore refactor: remove all PDF handling for HTML/CSV rework 2026-10-02 21:19:07 +00:00
go.mod build(deps): bump all Go dependencies to latest 2026-10-07 12:39:30 +00:00
go.sum build(deps): bump all Go dependencies to latest 2026-10-07 12:39:30 +00:00
LICENSE chore: license the project under AGPL-3.0 2026-10-03 12:20:06 +00:00
main.go refactor: move the CLI entrypoint to the repo root 2026-10-03 07:23:14 +00:00
README.md Merge pull request 'feat(server): web UI to upload Primavera timesheets and download processed ZIP' (#12) from feat/issue-11-web-ui-serve into main 2026-11-02 19:47:08 +01:00

timesheet-manager

Summarize weekly Primavera timesheet HTML reports into monthly summaries, written as HTML or CSV.

Scope

  • Identify .html files in a directory as Primavera timesheet reports, based on HTML content markers (not filenames).
  • Parse an identified report into an in-memory week: employee, timesheet period, status, per-hour activity rows, day totals, grand total, and time off. Period and row dates are accepted month-first (Aug-30-2026) or day-first (30-Aug-2026), and a trailing time (e.g. 08:00) in a date value is ignored.
  • Rename reports to the canonical filename pattern.
  • Summarize the weeks into a monthly total, splitting weeks that cross a month boundary by calendar day, and write the summary as HTML or CSV.
  • Serve the same rename/summarize pipeline from a local web UI: upload reports in the browser and download the results as a ZIP.

Usage

timesheet-manager <command> [options]

rename

timesheet-manager rename <dir> [--dry-run] [--verbose]

Scans dir for Primavera timesheet HTML reports (identified by content, not filename; only .html files are examined) and renames each to the canonical Timesheet Report <YYMMDD>-<YYMMDD>.html, derived from the parsed timesheet period — never from the existing filename.

  • The rename is idempotent: a file already carrying its canonical name is reported as a no-op and left untouched.
  • With --dry-run, nothing is written; planned renames are printed as would rename <old> -> <new>.
  • A rename that would overwrite an existing file, or that would collide with another report's canonical name, is reported as an error and skipped (fail-safe).
  • An identified report that fails to parse is reported as an error; the command still processes the remaining reports and exits non-zero if any errors occurred.

The command prints a summary line of the form rename: <renamed> renamed, <no-ops> no-op, <skipped> skipped, <errors> errors.

summarize

timesheet-manager summarize <dir> [--month YYYY-MM] [--output <path>] [--format html|csv] [--dry-run] [--partial] [--verbose]

Parses every identified timesheet report in dir, aggregates the in-month hours (weeks crossing a month boundary are split by calendar day), and writes monthly summary file(s).

  • Month selection: --month YYYY-MM summarizes exactly that month. Without --month, every month touched by the source reports' periods is discovered and summarized in ascending order — one summary per month that passes the coverage gate; a month failing the gate is skipped with a note on stderr (or generated with the partial-coverage notice when --partial is given).
  • Format selection: --format html or --format csv picks the output format. A --output path ending in .html or .csv selects the format by extension; an explicit --format that disagrees with the extension is an error. The default format is html.
  • Default output: <dir>/Timesheet Summary <YYMM>.html or .csv (e.g. Timesheet Summary 2609.html), one per summarized month, unless --output is given. --output is only valid when exactly one summary is written; otherwise the command errors and writes nothing — pass --month YYYY-MM to pick one.
  • With --dry-run, nothing is written; the command prints would write <path> (billable ... h, time off ... h).
  • Any identified report that fails to parse aborts the summary: the command prints the failing path(s) and exits non-zero, rather than writing a partial-month summary that understates the totals.
  • Without --partial, an explicitly selected month (--month) that is not fully covered makes the command exit non-zero; with --partial, the summary is generated anyway and carries a partial-coverage notice. In month-discovery mode (no --month) the command keeps going instead: incomplete months are skipped with a note on stderr and do not affect the exit code.
  • If no timesheet reports are found, the command exits non-zero with no timesheet reports found in <dir>.
  • summarize assumes one employee per directory: the summary is attributed to the employee of the parsed weeks; mixed employees are not detected.

The HTML summary includes a per-week Weekly contribution table whose Source column records each week's traceability as <filename> (<7-char sha256>) — the source report's filename plus the first 7 characters of its SHA-256 digest. The CSV summary has the header Project ID,Activity ID,Activity Name,Hours, one row per activity, and a final Total row, with LF line endings.

serve

timesheet-manager serve [--port <port>] [--verbose] (default port: 8080)

Serves a local web UI at http://localhost:<port>/ (default port 8080, bound to loopback only) that runs the same rename/summarize pipeline: timesheet HTML files are uploaded by drag & drop or file picker, options are chosen, and the results are downloaded as a ZIP.

  • The UI has two checkboxes: rename (on by default) renames each report to its canonical filename before packaging, mirroring the rename command; partial summaries (off by default) includes summaries for months not fully covered by the uploaded reports, each carrying the partial-coverage notice, mirroring summarize --partial.
  • The download is timesheet-manager-results-<timestamp>.zip containing the uploaded report files (renamed to Timesheet Report <YYMMDD>-<YYMMDD>.html when the rename checkbox is on) plus one Timesheet Summary <YYMM>.html and one Timesheet Summary <YYMM>.csv per month that passes the coverage gate.
  • Uploaded files and intermediate work live in a per-request session temp directory that is removed as soon as the request completes or the client disconnects; a background sweep removes anything left behind (sessions idle for more than 30 minutes, checked every 10 minutes).
  • Requests are always logged to stdout, and the server prints serve: listening on http://localhost:<port> on startup in both modes. Without --verbose each request line carries the method, status, timing, and client IP, and successful process requests additionally log the upload's file count and total bytes — never the request path or query string, upload file names, or upload contents. With --verbose the request path and per-file processing metadata (file names, skipped months, per-file validation reasons) are logged as well. Uploaded bodies are never logged in either mode. See docs/decisions/0003-web-server.md for the framework and upload-lifecycle choices.

Canonical filename patterns

  • Timesheet reports: Timesheet Report <YYMMDD>-<YYMMDD>.html (period start–end, e.g. Timesheet Report 260802-260808.html).
  • Monthly summaries: Timesheet Summary <YYMM>.html or Timesheet Summary <YYMM>.csv (e.g. Timesheet Summary 2609.csv).

Development

go build ./...
go vet ./...
go test ./...
  • Go version: 1.27+ (see go.mod).
  • Parsing uses golang.org/x/net/html; HTML summary output uses html/template; CSV output uses encoding/csv.
  • Library choices: see docs/decisions/0002-html-libraries.md (superseding docs/decisions/0001-libraries.md).
  • Tests are self-contained: they build synthetic HTML strings in code, with no test fixtures and no identifying data.

License

AGPL-3.0 — see LICENSE.