Skip to content

feat(gears): read the delivery board and show it as four Development lenses - #3014

Draft
hello1101n wants to merge 23 commits into
mainfrom
feat/gears-roadmap-view
Draft

feat(gears): read the delivery board and show it as four Development lenses#3014
hello1101n wants to merge 23 commits into
mainfrom
feat/gears-roadmap-view

Conversation

@hello1101n

@hello1101n hello1101n commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Part of #2906.

Why. Gear delivery is tracked on a GitHub project board and read through a spreadsheet run by hand, so nothing in Insight answers where a gear stands or what is late.

What changed. An admin-gated GET /v1/gear-roadmap?project=<n> reads the board out of bronze — the board's own fields joined to the issues carrying milestone, assignee and estimate — and four Development lenses render it: a per-subsystem rollup, the gear list, a month grid, and a schedule of what is left.

The caller names the board, and GET /v1/gear-roadmap/boards lists the ones bronze holds. It began as deployment config, which only compose ever set, so every Helm and GitOps install answered 412 while four more config surfaces waited to be plumbed. Which board exists is a fact about the synced data, so the data answers it. One board is read without asking; a second turns on a picker.

Capacity is assumed at one man-day per person per day, and every pane says so. Real capacity lives outside Insight, so the schedule states its assumption rather than presenting it as measured.

Ordering belongs to the endpoint. sort and direction are query parameters, an unknown column is refused, and gears carrying no value sort last in both directions — "unestimated" is not a small number.

Delivered is its own placement. Done on the board or closed on the tracker settles it, so finished work never reads as overdue, carries no remaining effort, and counts as done in every pane alike.

Worth knowing before you read the diff

  • A milestone names a month, so days late count from the day after that month ended.
  • Milestone, assignee and estimate come from bronze_github.issues, not the board item's own field values: the connector returns mirror-typed values as bare typenames.
  • A board is org-scoped, so an issue number repeats across repositories. A gear is named {repository}#{issue} end to end — the number alone collides.
  • project_items is a ReplacingMergeTree, so is_archived is resolved with argMax and filtered after. Filtering it first drops the archived latest and keeps a stale active row.
  • presentation_ro gets its bronze_* grants from provision-presentation-access.sh, which enumerates them on every deploy. A newly synced bronze database becomes readable at the next one.

Open after this

  1. A gear with several assignees is scheduled against the first one alone, who then carries all of its remaining effort. Every other pane lists them all.
  2. The board choice lives in component state, not the URL, so it does not survive a shared link. It cannot matter until an install syncs a second board.

Out of scope. Modelling the board into a granted database, and moving the rollup into metric definitions once org-grain metrics land.

Verified. 859 backend and 2454 frontend tests on this branch; clippy, eslint, tsc, rustfmt, and the OpenAPI and generated-model drift gates clean. Every lens driven in a browser against a local stand. No screenshots — this repo bans production-derived content and the stand holds real board rows.

…ty schedule

The board's status ladder, milestones and estimates live across two bronze
relations; this reads them as one row set and answers /v1/gear-roadmap with
both the per-gear view and a lane schedule.

Capacity is assumed at one man-day per person per day and stated in the
response, so a consumer renders the assumption rather than hiding it. A
milestone that has already passed is placed as overdue rather than folded
into the backlog, and an unassigned gear takes a lane of its own instead of
queueing behind every other unassigned one.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
Four views over the board endpoint: a per-subsystem rollup, the gear table,
a month grid and the assumed-capacity schedule.

The grid keeps overdue work in its own column rather than mixing it with
unscheduled work, and every view states the capacity the schedule assumed
instead of presenting it as measured.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
The project number is deployment-specific, so it arrives as a config leaf
rather than a literal. Non-positive means no board is named and the endpoint
answers with a failed precondition instead of an empty roadmap.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
The pages were standalone routes reached from the legacy sidebar, so they sat
outside the shell every other surface lives in. They are now a portal rail
zone with four pane items, dispatched by ZoneContent like the other zones, and
the legacy sidebar entry is gone.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
A rail zone of its own put the board beside the org zones, which is not what
it is: gear delivery is part of the Development direction. The lens registry
gains a board entry kind for a lens whose content is the board rather than
metric sections, so it contributes no measure keys to the direction's person
grid.

The schedule also gets the treatment its data deserves: a month axis with
gridlines, a sticky assignee column, one colour per subsystem, and labels that
truncate instead of overflowing. The grid and both tables pick up the same
colour key, aligned numerals, readiness meters and a totals row.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
A bar was a coloured rectangle with a native tooltip; hovering one now opens
a card naming the gear, the days it was scheduled for, what is left of its
estimate, its implementation share and its milestone.

Issue and account links resolve through the external-source registry rather
than a hardcoded host, so a deployment that configures no source renders plain
text instead of a wrong link. The registry gains account_href for the account
page a git host puts under a stable path; trackers resolve to nothing.

The gear table gains one toggle per subsystem, counted, colour-keyed and
combinable with the text filter — an empty choice means every subsystem.

Also fixes a read that could not decode: the repository column comes through
the CTE as a GROUP BY key, so it keeps its source nullability and needs a
default in the projection, which a guard test now states.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
…rson

The subsystem filter was a row of toggles as wide as the board had prefixes,
which read as content rather than as a control. It is now a single select
beside the search box, carrying each subsystem's colour and gear count.

Schedule lanes name a login, so the account page is resolved through a gear
that person is on — that is where the source id is recorded. A lane with no
configured source stays plain text.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
Four cleanup passes over the feature.

The lens view now owns the query and the pending/error branch; the four boards
take resolved data. They had each re-run the same query and re-implemented the
branch, and the copies had already drifted apart.

Link-or-text, the assignee list and the bar-plus-percentage widget were written
two to four times each; they are now the existing RecordLink plus two small
shared parts. Man-days and percentages go through lib/format's helpers and its
absent-value sentinel instead of ad hoc toFixed and a bare dash. Month
stepping uses date-fns, as the timeseries model already does, rather than a
third hand-rolled UTC loop.

A gear's placement is one tagged value instead of a label plus a slot that only
one label could carry, so a reader narrows once and an overdue gear cannot hold
a month index. A bar keeps its dates only and the view derives its geometry.

Two efficiency shapes: the subsystem rollup pushes instead of rebuilding each
group's array per gear, and lane account links read one login-to-source index
instead of scanning every gear per lane.

The lens registry gains one exported guard for entries that carry no metric
sections, replacing three ad hoc shape checks — including the second one bolted
onto an existing condition.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
… late

The two odd lens names are now parallel with the rest: Gear summary and Gear
list say what shape they are, the way Gear roadmap and Gear schedule do. Three
locale keys the portal move had orphaned are gone with them.

Ordering belongs to the endpoint, not the table: /v1/gear-roadmap takes sort
and direction, refuses a column it does not know, and puts gears carrying no
value last in both directions. Milestone order leads with the worst overrun,
because an overdue gear is not merely an earlier month. The column and its
direction ride in the URL, so an ordered view is a link.

A summary row opens the gear list narrowed to that subsystem, and the chosen
subsystem is in the URL too, which makes any filtered list linkable.

A milestone names a month, so overdue counts days from the day after that
month ended, and the count rides on the overdue placement itself — no other
state can carry one.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
…ss boards

A column now cycles largest-first, smallest-first, off, so a reader puts a
table back the way they found it with the control they already used. Cleared
means the request carries no order at all and the server's own applies.

Milestone was doing two jobs in one column. It now says only what the board
promised — with how many days it is already past — and a Forecast column says
which month the schedule lands the gear in, marked when that is later than the
promise. That is the split the spreadsheet makes, and the forecast comes from
the schedule that already runs for the lanes.

The subsystem in the URL now reaches every board, not just the list: the
roadmap grid narrows to it and the schedule keeps the whole board's span while
drawing only the bars that subsystem owns. Overdue chips in the grid carry
their day count too.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
Placement compared a gear's milestone to today and never asked whether the
work was done, so every delivered gear whose month had passed was counted late
— red in the grid's overdue column and marked as days late in the list.

Delivered is now its own placement, set when the board says Done or the
tracker says closed, since the two disagree in practice. The grid gives it a
column of its own rather than dropping it, so finished work stays visible as
history instead of vanishing or masquerading as a miss.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
It named a source id no sync produces, so it resolved no links for anyone but
the machine that stamped that id by hand.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
…view

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>

# Conflicts:
#	src/backend/services/analytics/src/api/mod.rs
#	src/backend/services/analytics/src/api/openapi_tests.rs
#	src/frontend/src/lib/portal/portal-nav.ts
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Comment @coderabbitai help to get the list of available commands.

Clippy under -D warnings fails the test target with
unfulfilled_lint_expectations: three test modules expect unwrap_used
but only call expect(), and three expect both lints while calling
neither. Keep only the expectation each module actually fulfils.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
Written through scripts/ci/openapi_spec.py update so the on-disk form
matches what the drift gate compares against.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
tests/generate_schemas.py derives them from the committed analytics
OpenAPI document, which now describes the gear roadmap response.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
The board number was a config leaf only docker-compose set, so every Helm
and GitOps install deserialized 0 and the endpoint answered BOARD_UNSET.
Plumbing it through four more config surfaces would have kept a value the
data already knows.

GET /v1/gear-roadmap now takes a required project parameter, and
GET /v1/gear-roadmap/boards lists the boards bronze holds so the lens can
offer them. One board is read without asking; a second turns on a picker.

Also declares sort and direction on the operation, which the handler
accepted and the frontend sent but the contract never described.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
…e board

project_items is a ReplacingMergeTree keyed per card, so an archived
re-read and its older active version coexist until a merge. Filtering
is_archived in the WHERE dropped the archived latest and grouped the
stale active row, so an archived card came back until ClickHouse merged.
Resolve the flag with argMax and filter the resolved value instead.

The issues expression carried no predicate, so it aggregated every issue
of every repository and tenant before the join narrowed it. Restrict it
to the repositories the board actually references.

Cap the read, matching the connector-health precedent: an unbounded
response is a bug however unlikely the input, and reaching the cap logs
rather than silently serving a partial board.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
A gear closed on the tracker below 100% on the board was delivered to the
scheduler — no lane, no forecast — while remaining_man_days still billed
the unfinished ladder, and the frontend summary counted done by percent
alone. Both now read the delivered state the rest of the panes read.

The gantt built dates at UTC midnight and then did month arithmetic in
the host's local calendar. West of UTC that midnight is the previous
local day, so the labels started a month early and could skip a month
outright. The arithmetic no longer touches a zone-bearing Date.

The test timezone was pinned to Kiritimati at UTC+14, where a UTC
midnight is still the same local day and the mixed implementation reads
as correct. Pinned west of UTC instead, which fails on the old code, and
covered monthLabels, which had no test at all.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
The handler calls require_admin on every gear roadmap read, but the four
lenses were listed for anyone who could reach Directions, so a non-admin
manager saw four ordinary navigation entries that answered 403 and
rendered the generic load failure.

The lens list is bare strings with no metadata slot, so the gate reads
the registry entry instead: a board lens is exactly one carrying a board,
which is also exactly the set the backend admin-gates.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
…lone

A GitHub project board is org-scoped, so repo-a#7 and repo-b#7 can both
sit on it. The forecast map, the schedule lookup and the React keys all
used the issue number, so one card's forecast and schedule bar silently
stood in for the other's and two rows shared a key.

The row already carried repo_full_name and the wire contract dropped it.
Gears now carry {repository}#{issue} end to end: the response keys on it,
the schedule resolves bars through it, and the tables key rows by it.
The number stays on the DTO because it is what a reader recognises.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
…board number as non-null

Two faults that only a running stand shows, both green through CI.

The gear roadmap registered its response DTO as GearDto, which the
gear-orchestrator gear already owns — a name it took when this branch
merged main. The host panics on the collision at boot and never binds,
so every request 502s. The offline OpenAPI test and both drift gates
build the analytics route table alone and never load the orchestrator,
so none of them can see it. Renamed to RoadmapGearDto, and the contract
now asserts it claims no name a host gear owns.

The boards read projected toInt64(project_number) while the column is
nullable, so the row decoder refused it and the endpoint 500'd on every
call. The predicate already excludes nulls; the projection now says so,
guarded the same way the gear read guards its repository column.

Signed-off-by: hello1101n <hello1101n@users.noreply.github.com>
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