diff --git a/apps/api/src/routes/v2/error-issues.http.ts b/apps/api/src/routes/v2/error-issues.http.ts index 1dae016783..6b41b49b05 100644 --- a/apps/api/src/routes/v2/error-issues.http.ts +++ b/apps/api/src/routes/v2/error-issues.http.ts @@ -151,6 +151,9 @@ export const HttpV2ErrorIssuesLive = HttpApiBuilder.group(MapleApiV2, "errorIssu endTime: query.end_time, introducedAfter: query.introduced_after, actionable: query.actionable === "true", + stateGroup: query.state, + search: query.search?.trim() || undefined, + includeArchived: query.include_archived === "true", sort, limit: query.limit ?? 20, cursor, diff --git a/apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json b/apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json index 153e120a84..b24ee4676a 100644 --- a/apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json +++ b/apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json @@ -5393,6 +5393,38 @@ "type": "string" } }, + { + "in": "query", + "name": "state", + "required": false, + "schema": { + "enum": [ + "open", + "closed" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "search", + "required": false, + "schema": { + "maxLength": 200, + "type": "string" + } + }, + { + "in": "query", + "name": "include_archived", + "required": false, + "schema": { + "enum": [ + "true" + ], + "type": "string" + } + }, { "in": "query", "name": "sort", diff --git a/apps/web/src/components/errors/error-signal-row.tsx b/apps/web/src/components/errors/error-signal-row.tsx index bc70b8d3c9..32954b0f0d 100644 --- a/apps/web/src/components/errors/error-signal-row.tsx +++ b/apps/web/src/components/errors/error-signal-row.tsx @@ -180,7 +180,7 @@ export function ErrorSignalHeader({ ) : null} - Error + Issue Trend · 24h Events diff --git a/apps/web/src/components/errors/errors-hub-view.tsx b/apps/web/src/components/errors/errors-hub-view.tsx index 7bdad7569b..fa5e8ffcb3 100644 --- a/apps/web/src/components/errors/errors-hub-view.tsx +++ b/apps/web/src/components/errors/errors-hub-view.tsx @@ -4,6 +4,7 @@ import { useNavigate } from "@tanstack/react-router" import type { ErrorIssueId, WorkflowState } from "@maple/domain/http" import { Button } from "@maple/ui/components/ui/button" +import { ToolbarSearch } from "@maple/ui/components/toolbar" import { LoadMoreButton } from "@maple/ui/components/ui/list-footer" import { SkeletonList } from "@maple/ui/components/ui/skeleton" import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@maple/ui/components/ui/empty" @@ -63,37 +64,22 @@ const SKELETON_ROWS = 8 const VIEW_LABEL: Record = { open: "Open", - triage: "Triage", - active: "Active", - resolved: "Resolved", + closed: "Closed", all: "All", } satisfies Record /** - * Which workflow states each view covers. `triage` is the untouched queue, - * `active` is everything someone has picked up, `resolved` is closed work. - * `regressed` sits in triage because a fix that did not hold is untriaged - * again, not in-progress. - * - * `open` is the union of triage and active, spelled out rather than left as - * `"all"`. The SERVER already narrows it — `actionable=true` maps to exactly - * these five states — so on the real page the client filter passes every row it - * is given and discards nothing. Spelling it out is what lets the view be - * rendered off a fixture (`/lab/errors`) and still show the set the server - * would have returned. + * Which workflow states each view covers. The server applies the same sets + * (`state=open|closed`); spelling them out here lets `/lab/errors` render a + * view off a fixture, and drops a row the moment a triage action moves it out. + * `verifying` is Closed: the fix has merged and the tick, not a person, acts next. */ const VIEW_STATES = { open: ["triage", "regressed", "todo", "in_progress", "in_review"], - triage: ["triage", "regressed"], - active: ["todo", "in_progress", "in_review"], - resolved: ["done", "cancelled", "wontfix"], + closed: ["verifying", "done", "cancelled", "wontfix"], all: "all", } satisfies Record | "all"> -/** Views the server can narrow with `actionable=true`, so the page it returns - * is already the right one. */ -export const ACTIONABLE_VIEWS: ReadonlyArray = ["open", "triage", "active"] - /** Membership against the view's state set. The literal tuples above keep their * inferred element types, which do not accept an arbitrary `WorkflowState` as * an `includes` argument — widening happens here, once. */ @@ -200,27 +186,17 @@ const EMPTY_COPY = { open: { icon: CircleCheckIcon, title: "Nothing open", - description: "Every error here is closed out. Check Resolved to see recent fixes.", - }, - triage: { - icon: CircleCheckIcon, - title: "Nothing waiting on triage", - description: "Every error here has been picked up.", - }, - active: { - icon: CircleCheckIcon, - title: "Nothing in progress", - description: "No one has claimed an error yet. Start from the triage queue.", + description: "Every issue here is closed out. Check Closed to see recent fixes.", }, all: { icon: CircleCheckIcon, title: "No errors recorded", description: "Your services are sending traces and none have failed.", }, - resolved: { + closed: { icon: HistoryIcon, - title: "Nothing resolved yet", - description: "Errors you close land here, so you can check whether a fix held.", + title: "Nothing closed yet", + description: "Issues you close land here, so you can check whether a fix held.", }, } satisfies Record @@ -240,6 +216,9 @@ export interface ErrorsHubViewProps { onViewChange: (view: HubView) => void onSortChange: (sort: HubSort) => void onSeverityChange: (severity: SeverityFilter) => void + /** The applied search term, matched server-side. */ + search: string + onSearchChange: (search: string | undefined) => void /** The window totals above the toolbar. A slot rather than a component * because it is the one part of the list that fetches for itself. */ stats?: React.ReactNode @@ -260,6 +239,8 @@ export function ErrorsHubView({ onViewChange, onSortChange, onSeverityChange, + search, + onSearchChange, stats, paging, onClearFilters, @@ -278,14 +259,21 @@ export function ErrorsHubView({ tabs={HUB_VIEWS.map((value) => ({ value, label: VIEW_LABEL[value] }))} active={view} label="Filter errors" - countNoun={["error", "errors"]} + countNoun={["issue", "issues"]} /* No count until there is one to state. "0 errors" under a spinner is a claim about the data, and it is wrong every time. */ totalCount={status === "ready" ? sorted.length : undefined} - countLabel={status === "ready" && hasUnshownPages ? `${sorted.length}+ errors` : undefined} + countLabel={status === "ready" && hasUnshownPages ? `${sorted.length}+ issues` : undefined} onChange={onViewChange} trailing={ <> +