Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fixed-10697.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- dashboard: Overview 'Actionable now' shows the exact reconciling equation; drop median-age tile (#10697)
32 changes: 16 additions & 16 deletions src/docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ Auth levels are derived from dashboard middleware (`isPublicPath`, dashboard tok
|---|---|---|---|---|
| `GET` | `/api/version` | Dashboard auth/session | Build/version metadata; includes `upgradeMarker` (`target`, `current`, `attempts`, `maxAttempts`, `failed`, `requestedAt`, `lastError`) while a self-upgrade is in flight or has failed ([#6765](https://github.com/hivecommons/hive/issues/6765)), the hub-delivered `upgradePolicy` (`hub_managed`, `spoke_managed`, `schedule`, `schedule_hour`, `schedule_timezone`, `schedule_weekday`, `paused`, `branch`, `channel`, `target_sha`, `target_resolved`, `armed_target`, and `next_update_at` β€” the hub's RFC3339 ETA for the next promotion into the `stable` channel, omitted when unknown ([#10256](https://github.com/hivecommons/hive/issues/10256))) when available, and an `autoUpdate` object (`enabled`, `state` β€” one of `disabled`/`up_to_date`/`behind`/`retrying`/`failed`/`unknown` β€” `healthy`, `period`, `targetBranch`, `targetCommit`, `currentCommit`, `commitsBehind`, `lastAttemptAt`, `lastError`, `detail`, and `nextUpdateAt` β€” the hub's RFC3339 ETA for the next update, omitted when unknown, disabled or paused ([#10257](https://github.com/hivecommons/hive/issues/10257))) that never reports a failed or unknown update as healthy ([#6962](https://github.com/hivecommons/hive/issues/6962), [#6963](https://github.com/hivecommons/hive/issues/6963)) | `pkg/dashboard/api.go:36` |
| `POST` | `/api/release-channel` | Owner only | Hosted spoke self-service release-channel selector; relays `stable`/`candidate`/`edge` to the hub's existing switch-branch endpoint with the spoke dashboard-token proof, and reports the requested channel as pending until the Deployment image lands | `pkg/dashboard/api.go:57` |
| `GET` | `/api/health` | Public | Basic health probe | `pkg/dashboard/server.go:1213` |
| `GET` | `/api/health/deep` | Public | Deep health probe | `pkg/dashboard/server.go:1214` |
| `GET` | `/api/livez` | Public | Kubernetes liveness probe | `pkg/dashboard/server.go:1215` |
| `GET` | `/metrics` | Registered only when `HIVE_METRICS_ENABLED`; requires `Authorization: ****** (403 if the token is unset) | Prometheus metrics | `pkg/dashboard/server.go:1221` |
| `GET` | `/api/status` | Dashboard auth/session | Dashboard aggregate status; accepts `?fields=a,b` for top-level selection and `?omit=repos` to drop heavy sections, and honors `Accept-Encoding: gzip`; `agents[]` includes busy-pane visibility fields (`kicksUndeliverable`, `busySince`, `lastTranscriptActivity`, `condition`, `conditionMessage`) when a Working pane cannot accept kicks | `pkg/dashboard/server.go:1226` |
| `GET` | `/api/status/summary` | Dashboard auth/session | Compact agent/governor/budget status summary for pollers; honors `Accept-Encoding: gzip` | `pkg/dashboard/server.go:1227` |
| `GET` | `/api/overview/issues.csv` | Dashboard auth/session | Cached Overview issue-band export as browser-compatible CSV; filters `band`, `repo`/`repos`, `stale`, and `held` are ANDed. | `pkg/dashboard/server.go:1228` |
| `GET` | `/api/overview/issues.json` | Dashboard auth/session | Cached Overview issue-band export as typed JSON with `bands[]` metadata and row fields matching the CSV columns. | `pkg/dashboard/server.go:1229` |
| `GET` | `/api/overview/prs.csv` | Dashboard auth/session | Cached Overview PR-band export as browser-compatible CSV; filters `band`, `repo`/`repos`, `stale`, and `held` are ANDed. | `pkg/dashboard/server.go:1230` |
| `GET` | `/api/overview/prs.json` | Dashboard auth/session | Cached Overview PR-band export as typed JSON with `bands[]` metadata and row fields matching the CSV columns. | `pkg/dashboard/server.go:1231` |
| `GET` | `/api/events` | Dashboard auth/session | Server-sent event stream | `pkg/dashboard/server.go:1232` |
| `GET` | `/api/health` | Public | Basic health probe | `pkg/dashboard/server.go:1232` |
| `GET` | `/api/health/deep` | Public | Deep health probe | `pkg/dashboard/server.go:1233` |
| `GET` | `/api/livez` | Public | Kubernetes liveness probe | `pkg/dashboard/server.go:1234` |
| `GET` | `/metrics` | Registered only when `HIVE_METRICS_ENABLED`; requires `Authorization: ****** (403 if the token is unset) | Prometheus metrics | `pkg/dashboard/server.go:1240` |
| `GET` | `/api/status` | Dashboard auth/session | Dashboard aggregate status; accepts `?fields=a,b` for top-level selection and `?omit=repos` to drop heavy sections, and honors `Accept-Encoding: gzip`; `agents[]` includes busy-pane visibility fields (`kicksUndeliverable`, `busySince`, `lastTranscriptActivity`, `condition`, `conditionMessage`) when a Working pane cannot accept kicks | `pkg/dashboard/server.go:1245` |
| `GET` | `/api/status/summary` | Dashboard auth/session | Compact agent/governor/budget status summary for pollers; honors `Accept-Encoding: gzip` | `pkg/dashboard/server.go:1246` |
| `GET` | `/api/overview/issues.csv` | Dashboard auth/session | Cached Overview issue-band export as browser-compatible CSV; filters `band`, `repo`/`repos`, `stale`, and `held` are ANDed. | `pkg/dashboard/server.go:1247` |
| `GET` | `/api/overview/issues.json` | Dashboard auth/session | Cached Overview issue-band export as typed JSON with `bands[]` metadata and row fields matching the CSV columns. | `pkg/dashboard/server.go:1248` |
| `GET` | `/api/overview/prs.csv` | Dashboard auth/session | Cached Overview PR-band export as browser-compatible CSV; filters `band`, `repo`/`repos`, `stale`, and `held` are ANDed. | `pkg/dashboard/server.go:1249` |
| `GET` | `/api/overview/prs.json` | Dashboard auth/session | Cached Overview PR-band export as typed JSON with `bands[]` metadata and row fields matching the CSV columns. | `pkg/dashboard/server.go:1250` |
| `GET` | `/api/events` | Dashboard auth/session | Server-sent event stream | `pkg/dashboard/server.go:1251` |
| `GET` | `/api/swarm` | Dashboard auth/session | Current swarm status: display name, duration, active repo, expiry, persisted prep metrics when available, and idle-unlock fields. | `pkg/dashboard/swarm.go:310` |
| `POST` | `/api/swarm` | Owner only | Start a 24h repo swarm for a configured repo, returning 409 when another swarm is active and 423 when a previous swarm keeps the next swarm locked until enough agents are idle unless body `{"repo":"owner/name","force":true}` is used; saves the active record before repo prep, persists prep metrics, and announces the start to Discord when configured. | `pkg/dashboard/swarm.go:311` |
| `DELETE` | `/api/swarm` | Owner only | End the active swarm, score issues closed and PRs merged in the window, append it to history, and announce the result to Discord when configured. | `pkg/dashboard/swarm.go:312` |
Expand Down Expand Up @@ -62,7 +62,7 @@ Auth levels are derived from dashboard middleware (`isPublicPath`, dashboard tok
| Method | Path | Auth | Purpose | Source |
|---|---|---|---|---|
| `GET` | `/api/style` | Public | Sanitized custom dashboard CSS | `pkg/dashboard/api.go:37` |
| `GET` | `/branding/custom.css` | Dashboard auth/session | Operator branding stylesheet override, read per request (see [branding](branding.md)) | `pkg/dashboard/server.go:1293` |
| `GET` | `/branding/custom.css` | Dashboard auth/session | Operator branding stylesheet override, read per request (see [branding](branding.md)) | `pkg/dashboard/server.go:1312` |
| `GET` | `/api/snapshot/frame-ancestors` | Public | Snapshot framing allowlist | `pkg/dashboard/api.go:70` |
| `GET` | `/api/snapshot` | Public | Snapshot data | `pkg/dashboard/api.go:71` |
| `GET` | `/snapshot` | Public | Public read-only snapshot page | `pkg/dashboard/api.go:72` |
Expand Down Expand Up @@ -90,9 +90,9 @@ Auth levels are derived from dashboard middleware (`isPublicPath`, dashboard tok
| `GET` | `/api/openrouter/models` | Dashboard auth/session | Open Router Models | `pkg/dashboard/openrouter.go:47` |
| `GET` | `/api/openrouter/credit` | Dashboard auth/session | Open Router Credit | `pkg/dashboard/openrouter.go:48` |
| `GET` | `/openrouter/callback` | Public | Open Router Callback | `pkg/dashboard/openrouter.go:49` |
| `POST` | `/api/github-app/recheck` | Dashboard auth/session | GitHub App Recheck | `pkg/dashboard/server.go:1233` |
| `POST` | `/api/github-app/install-clicked` | Dashboard auth/session | GitHub App Install Clicked | `pkg/dashboard/server.go:1234` |
| `GET` | `/gh-setup` | Public | GitHub App Setup Callback | `pkg/dashboard/server.go:1235` |
| `POST` | `/api/github-app/recheck` | Dashboard auth/session | GitHub App Recheck | `pkg/dashboard/server.go:1252` |
| `POST` | `/api/github-app/install-clicked` | Dashboard auth/session | GitHub App Install Clicked | `pkg/dashboard/server.go:1253` |
| `GET` | `/gh-setup` | Public | GitHub App Setup Callback | `pkg/dashboard/server.go:1254` |

## Configuration

Expand Down Expand Up @@ -539,7 +539,7 @@ always resolved server-side from the validated token.
| `POST` | `/api/hives/{id}/heartbeat` | Dashboard auth/session | Hives Heartbeat | `pkg/dashboard/api_contribute.go:263` |
| `DELETE` | `/api/hives/{id}` | Owner only | Hives Delete | `pkg/dashboard/api_contribute.go:264` |
| `POST` | `/api/hives/onboard` | Dashboard auth/session | Hives Onboard | `pkg/dashboard/api_contribute.go:265` |
| `GET` | `/sso` | Public | SSO | `pkg/dashboard/server.go:1240` |
| `GET` | `/sso` | Public | SSO | `pkg/dashboard/server.go:1259` |

## Hub SaaS

Expand Down
39 changes: 25 additions & 14 deletions src/docs/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ posture, and cadence controls for the hive. Its collapsible **PRs by model** nes
sub-section reads `GET /api/governor/pr-models` with the selected `7d`, `30d`,
or `all` window. The nested header keeps the window/sort toggles visible and its
collapsed summary shows the current top-ranked model plus model count.

The Governor **actionable issues** and **actionable PRs** tiles use the shared
server-side actionable partition described in [Repository card legend, issue
bands, and PR bands](#repository-card-legend-issue-bands-and-pr-bands). Their
subtitles show the per-kind reconciliation from total open work to the
actionable split, and those two results always add back to the Overview
**Actionable now** total.

The cadence table starts each agent row with the same `1`/`0` Agent power
rocker used in the agent settings panel, so owners can enable or disable an
agent from the table; disabled rows are dimmed, and read-only viewers see the
Expand Down Expand Up @@ -196,18 +204,21 @@ client-side issue and PR bands across the selected repository view. Its SVG
charts reuse the repository-card classifiers for actionable plus held
issues/PRs, so their totals match the visible band counters and respect the
Overview settings repo filter without a separate API call. A compact KPI strip
shows total open issues, total open PRs, actionable now, held,
blocked/needs-human, and the median actionable age. **Total open issues** and
**Total open PRs** are forge totals for the selected configured repositories
from the existing scanner tick: open GitHub issues exclude pull requests, while
open GitHub pull requests include drafts. Their muted sub-lines name how many
items are present in the Overview's actionable/held bands and how many were
outside those bands because of hard-suppress labels, exempt filters, reporter
triage, standing meta issues, dependency dashboards, drafts, or other scanner
filters. **Actionable now** is the shared server-side count used by
the Overview KPI and the Governor collapsed split: open enumerated issues plus
open enumerated PRs after hold/exempt filters, excluding items in the waiting,
done, draft, or blocked Overview bands. Its `actionableNow.issues` and
shows total open issues, total open PRs, actionable now, held, and
blocked/needs-human. This is the same partition used by the Governor
actionable issue/PR subtitles. **Total open issues** and **Total open PRs** are
forge totals for the selected configured repositories from the existing scanner tick:
open GitHub issues exclude pull requests, while open GitHub pull requests
include drafts. Their subtitles stay empty so the reconciliation is not split
across tiles. **Actionable now** is the shared server-side count used by the
Overview KPI and the Governor collapsed split: open enumerated issues plus open
enumerated PRs after hold/exempt filters, excluding items in the waiting, done,
draft, or blocked Overview bands. Its subtitle carries the complete partition,
for example `41 issues + 17 PRs = 13 actionable + 13 held + 10
blocked/needs-human + 22 outside`; the held and blocked/needs-human tiles use
the same server-side terms, and outside covers scanner-filtered work such as
hard-suppress labels other than needs-human, exempt filters, reporter triage, standing meta issues,
dependency dashboards, drafts, or other filters. Its `actionableNow.issues` and
`actionableNow.prs` splits always sum to `actionableNow.total`. Operators can
view each Issues or PRs panel as a donut,
pie, horizontal bar, single 100% stacked bar, line/spark trend, or age
Expand All @@ -231,12 +242,12 @@ pages include their API token in the download link's query string.
The Overview header's βš™οΈ popover stores browser-local chart preferences under
`hive-overview-charts`: which chart types are in rotation, whether the carousel
is enabled, the 5-second to 5-minute interval, transition style, duration, donut
label mode, KPI visibility, export format, default age basis, and the bounded
label mode, KPI visibility, export format, and the bounded
client-side line/spark history. The repo multi-select is stored separately under
`hive.overview.repos`, with All/None shortcuts and an Org shortcut when the
hive spans multiple GitHub organizations. The default remains donut-only with
the carousel off, a 30-second interval, fade transition, normal duration, KPI
strip on, CSV exports, updated-time age basis, and all repos selected.
strip on, CSV exports, and all repos selected.
Manual arrows and dot indicators are available even when timed rotation is off;
timed rotation pauses while the panel is hovered or the tab is hidden, and
reduced-motion users get instant swaps.
Expand Down
3 changes: 3 additions & 0 deletions src/pkg/dashboard/actionable_now_10561_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ func TestOverviewAndGovernorBindSharedActionableNowField10561(t *testing.T) {
"const sharedActionable = (data && data.actionableNow) || {};",
"sharedActionable.issues",
"sharedActionable.prs",
"sharedActionable.issueEquation",
"sharedActionable.prEquation",
"issueEquationText ? `<div class=\"overview-kpi-subline\">${escapeHtml(issueEquationText)}</div>` : ''",
} {
if !strings.Contains(html, want) {
t.Fatalf("dashboard missing shared actionable binding %q", want)
Expand Down
39 changes: 9 additions & 30 deletions src/pkg/dashboard/overview_kpi_sparklines_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,9 @@ func TestOverviewKPIHistoryAppendCapPersistRestoreDownsample(t *testing.T) {
t.Fatalf("history len = %d, want cap %d", len(got), trendHistoryMaxEntries)
}
last := got[len(got)-1]
if last.OverviewOpenIssues != 3 || last.OverviewOpenPRs != 1 || last.OverviewActionable != 1 || last.OverviewHeld != 2 || last.OverviewBlockedHuman != 2 {
if last.OverviewOpenIssues != 3 || last.OverviewOpenPRs != 1 || last.OverviewActionable != 1 || last.OverviewHeld != 2 || last.OverviewBlockedHuman != 1 {
t.Fatalf("overview KPI sample = %+v", last)
}
lastAt := start.Add(time.Duration(trendHistoryMaxEntries+4) * timeHistoryStep())
wantMedianAgeSec := int(lastAt.Sub(start.Add(-1 * time.Hour)).Seconds())
if last.OverviewMedianAgeSec != wantMedianAgeSec {
t.Fatalf("median age sec = %d, want %d", last.OverviewMedianAgeSec, wantMedianAgeSec)
}

dir := t.TempDir()
path := filepath.Join(dir, "overview-history.json")
data, err := json.Marshal(got)
Expand Down Expand Up @@ -95,20 +89,6 @@ func TestOverviewKPIHistoryJSONIncludesZeroOverviewFields(t *testing.T) {
if strings.Contains(string(data), `"overviewMedianAgeSec"`) {
t.Fatalf("marshaled overview history reported unavailable median as zero: %s", data)
}

at := time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC)
withMedian := &Server{}
withMedian.appendTrendHistoryAt(&StatusPayload{Repos: []FrontendRepo{{
Name: "hive",
ActionableIssues: []any{github.Issue{UpdatedAt: at}},
}}}, at)
data, err = json.Marshal(withMedian.OverviewKPIHistory(0))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(data), `"overviewMedianAgeSec":0`) {
t.Fatalf("marshaled real zero median missing: %s", data)
}
}

func timeHistoryStep() time.Duration {
Expand All @@ -133,7 +113,7 @@ let _overviewLastRepos = [{name: 'hive', full: 'hivecommons/hive'}];
global.window = {_lastStatus: {hiveId: 'test'}};
global.localStorage = { getItem(){ return null; }, setItem(){}, removeItem(){} };
let _overviewKPIHistory = [
{t: Date.now(), overviewOpenIssues: 58, overviewOpenPrs: 12, overviewActionable: 36, overviewHeld: 6, overviewBlockedHuman: 3, overviewMedianAgeSec: 3600}
{t: Date.now(), overviewOpenIssues: 58, overviewOpenPrs: 12, overviewActionable: 36, overviewHeld: 6, overviewBlockedHuman: 3}
];
const OVERVIEW_ISSUE_BREAKDOWN_LABELS = { needs_human: 'needs-human', needs_direction: 'needs-direction', needs_decision: 'needs-decision', needs_spec: 'needs-spec', exempt: 'exempt', filtered: 'filtered', reporter_triage: 'reporter triage', hive_advisory: 'hive advisory', dependency_dashboard: 'dependency dashboard', other: 'other' };
const OVERVIEW_PR_BREAKDOWN_LABELS = { hold: 'held', draft: 'draft', filtered: 'filtered', other: 'other' };
Expand All @@ -144,11 +124,11 @@ function overviewItemAgeMinutes(){ return NaN; }
`)
for _, name := range []string{
"fmtSparkVal", "sparklineSeriesKey", "sparklineReducedMotion", "sparklineValueSummary", "renderSparkline",
"fmtDurationFromSeconds", "overviewMedianAgeSeconds", "overviewMedianAgeLabel", "overviewKPIRepoScope", "overviewKPIHistoryEntries",
"overviewKPIRepoScope", "overviewKPIHistoryEntries",
"overviewRepoName", "overviewAllRepoNames", "overviewSavedRepoNames", "overviewSelectedRepoNames", "overviewFilterRepos",
"overviewRepoNumber", "overviewRepoOpenIssueCount", "overviewRepoOpenPRCount",
"overviewBreakdownTotal", "overviewRepoForgeTotals", "overviewKPIForgeTotals", "overviewKPIBreakdownSubline",
"overviewKPISparkTitle", "overviewKPISpark", "overviewKPICurrentSample", "renderOverviewKPIs",
"overviewKPITerm", "overviewKPIEquation", "overviewKPISparkTitle", "overviewKPISpark", "overviewKPICurrentSample", "renderOverviewKPIs",
} {
source.WriteString(jsFunc(t, html, name))
source.WriteByte('\n')
Expand All @@ -161,14 +141,13 @@ const prSlices = [{key: 'ready', count: 12, items: []}, {key: 'blocked', count:
// the selected objects returned by overviewFilterRepos.
const liveRepos = [{name: 'hive', full: 'hivecommons/hive', issues: 39, prs: 31, actionableIssues: [1,2,3,4,5,6,7], heldIssues: [8,9], openPrs: [10,11], heldPrs: [12,13]}];
_overviewLastRepos = liveRepos;
const markup = renderOverviewKPIs(overviewFilterRepos(liveRepos), issueSlices, prSlices, {showKPIs: true, timeBasis: 'updated'});
assert.equal((markup.match(/class="overview-kpi"/g) || []).length, 6);
assert.equal((markup.match(/<svg/g) || []).length, 6);
const markup = renderOverviewKPIs(overviewFilterRepos(liveRepos), issueSlices, prSlices, {showKPIs: true});
assert.equal((markup.match(/class="overview-kpi"/g) || []).length, 5);
assert.equal((markup.match(/<svg/g) || []).length, 5);
assert.match(markup, />39<\/span><span class="overview-kpi-label">Total open issues<\/span>/);
assert.match(markup, />31<\/span><span class="overview-kpi-label">Total open PRs<\/span>/);
assert.match(markup, />β€”<\/span><span class="overview-kpi-label">Median actionable age \(updated\)<\/span>/);
assert.match(markup, /Median time since updated for actionable open issues and PRs in the selected repos \(not MTTR, which measures time-to-resolve for closed items\)\./);
for (const key of ['open-issues','open-prs','actionable-now','held','blocked-needs-human','median-age']) {
assert.match(markup, />39 issues \+ 31 PRs = /);
for (const key of ['open-issues','open-prs','actionable-now','held','blocked-needs-human']) {
assert.match(markup, new RegExp('overview-kpi:' + key));
}
`)
Expand Down
Loading
Loading