Reports
Five reports over one filter, and a way to keep a filter you use often.
The shape of it
Every report accepts the same filter: period, projects, types, statuses, priorities, specific people and agents, and whether to count humans, agents or both. Reports differ in what they aggregate, never in what they can be narrowed by.
That is deliberate. Five reports with five filter implementations is five places for "last quarter" to come to mean something slightly different.
What each one answers
| Report | The question |
|---|---|
| Status report | Where are we, what moved, what is stuck, what is due |
| Time report | Where the hours went, and whose they were |
| Throughput | Are we keeping up, and how long does work take |
| Workload | Who is carrying what, right now |
| Agent performance | Which agents are working, and which are failing |
Status report
Built for somebody who is not in the tool. Overdue and on-hold come first, because a status report is read for its problems.
Closings are read from the activity log rather than from updated_at: an issue
somebody comments on next week would otherwise slide out of the period it was
actually finished in.
Time report
Human time and machine time in the same table, split but never separated. An agent's twenty minutes is twenty minutes the team did not spend, and a report that hides it understates both the cost and the help.
Estimates are only compared where an estimate exists. An absent estimate is not a promise, and counting it as zero would invent an overrun nobody agreed to.
Throughput
Opened against closed, week by week. Whether the backlog is growing is a fact no board will ever show you, because a board only knows the present.
Cycle time is a median. One issue that sat in the backlog for a year should not define the team's reputation.
Workload
Deliberately not filtered by period — what somebody had on their plate last March is not a useful question.
Issues without an estimate are counted and shown rather than averaged away. A plate with three unestimated issues on it is not a light plate.
Agent performance
Success rate counts finished runs only. A run still in flight is not yet a success or a failure, and counting it as either is a lie that flatters.
Time has to be attributable
issues.spent_minutes is a cached total. Underneath it sits one entry per piece
of work, each with an actor, a date and a source:
- A person logs time from the issue's properties panel
- An agent writes an entry when its run completes
The date used everywhere is when the work happened, not when it was entered. An hour worked on Monday and logged on Friday belongs to Monday.
Agent entries cannot be deleted. They are a record of something that happened, not a claim somebody typed, and removing one would make the run history disagree with the time report.
Saved views
Narrow a report, then Save this view. It appears on the reports index and in the picker on that report.
Saved views are private by default. A saved filter is a working note until its author decides it is a team artefact, so sharing is a deliberate act rather than the fallback. A shared view can be opened by anyone in the workspace and changed only by whoever made it.
A view that has been deleted or unshared stops applying rather than breaking the page somebody bookmarked.
How they are computed
Aggregation happens in the database. The first version fetched every row in the period and grouped in PHP, which is fine for a fortnight and is a memory limit for All time — the one option in the picker nobody thinks twice about choosing.
Three things deliberately stay in PHP:
- Medians, which have no portable SQL. Only the two timestamp columns are read, never whole models.
- Weekly buckets, because grouping by week means
YEARWEEKon MySQL andstrftimeon SQLite. Days are grouped in SQL and bucketed in PHP: a year is at most 365 rows. - Name lookups, resolved in two queries rather than one per row.
The lists on a status report are capped at 200 rows, since the page shows twenty-five. The export builds its own and is not capped.
Every aggregate is exercised against both MySQL and SQLite in the test suite,
because conditional sums, DATE() and JSON paths are exactly where the two
engines diverge.
Export
Every report exports as CSV, honouring the filters on screen.
CSV rather than PDF because a spreadsheet is what people actually do with a report: re-sort it, total a column, paste it into something else. A PDF looks finished and can only be read.
The time export is one row per entry rather than a summary, so whoever opens it can group it whichever way they came for. It is streamed lazily — each row is written and forgotten, so a year of entries is never resident at once.
Exports lead with a byte-order mark. Without one, Excel reads UTF-8 as Latin-1 and turns Félagi into Félagi on the first row of every file.
Not there yet
- No scheduled reports. Nothing arrives in an inbox on a Monday morning.
- Cost is only as complete as your CLIs. Claude Code reports it; Elyra does not
yet, and a run that reported nothing shows
—rather than$0.00— a total says how many runs it covers, so a figure from three of forty cannot be multiplied out by mistake. - No currency other than USD, and no conversion. An agent hour is time rather than money.
- No per-project time budget, and no billable flag.