The status page¶
Patchy's human surface beyond the tracking issues: a web dashboard served by the
status-server showing every Finding in the namespace, the two human gates
(AwaitingApproval holds and HandedOff revival), and the all-time FindingRollup statistics. The page is a single
embedded artifact — one self-contained HTML file compiled into the binary — with live updates: the server watches the
Finding and FindingRollup resources and nudges open browsers over Server-Sent Events to refetch.
The same findings, the same actions and the same RBAC are available from a terminal via the patchy CLI — it reads and writes the identical custom resources, so the two surfaces never disagree about state.
Views¶
The screenshots below show the canned dev data — every finding
deliberately suspended, hence the suspended pill on each row.
- Findings — stat tiles, the eleven-phase lifecycle rail with live counts (click a phase to filter),
severity/verdict/repository/text filters, and the board: severity, phase (with a live-dot while an agent run is active
and a
suspendedpill), confidence, verdict, and age per finding.

- Finding detail — the advisory header with status and severity, tabs (Overview · Alerts · Timeline · Investigation · Remediation), the metadata sidebar (owners, repository, source, tracking issue, advisories, dates), and the action bar. A completed remediation shows the merged PR; terminal states surface failure reasons, dismissal verdicts, and hand-off routes. The investigation and remediation tabs each end in a Conversation section — see Agent conversations.

- Rollups — all-time statistics by total, repository, harness, and model scope: terminal-phase and verdict mixes, per-stage token/cost/duration aggregates, and the monthly trend. This view is public — it renders without signing in.

- Config — the namespace's configured Integrations, Forges, and derived context enhancers: provider, capabilities,
credential Secret names (names only, never contents), readiness, the failed-delivery sweep's last outcome, and the
manual backfill trigger with its last run's report. The nav link
renders only for users granted
getonintegrations— unlike rollups this view is never public, and it is read-only apart from the backfill trigger: the resources themselves are managed in the cluster.
Agent conversations¶
Each run's investigation and remediation tab carries the agent's turn-by-turn conversation: its messages, its reasoning, every tool it called and what came back. While a run is in flight the section streams live; afterwards it replays the stored record.
What makes that possible is that the agent pod normalises its own CLI's output — claude's content blocks, codex's items,
copilot's messages all become the same turn vocabulary — and emits it on stdout under a PATCHY-TURN: prefix, alongside
the PATCHY-EVENT: stage result. The owning controller reads that log once when the Job finishes and writes the
conversation to a ConfigMap owner-referenced to the run. Since a run is owned by its Finding, the
finding TTL cascade deletes the transcript with everything else: a
conversation is retained exactly as long as the finding it explains, and no longer.
Two limits are worth knowing:
- Turns are bounded and redacted. Each turn's text is capped (default 2 KiB), a run is capped at 500 turns and 512
KiB total, and any value matching a credential in the pod's environment is replaced with
«redacted»before it leaves the pod. A conversation cut short by those bounds says so, in the section footer and on the last turn. Override withPATCHY_TRANSCRIPT_MAX_TURN_BYTES,PATCHY_TRANSCRIPT_MAX_TURNS,PATCHY_TRANSCRIPT_MAX_TOTAL_BYTES. - The upstream session id is not a handle.
stage.sessionIDrecords what the agent CLI called its session, which is useful for correlating against model-provider logs — but it resolves only against a session file in the pod's$HOME, which is anemptyDirthat dies with the pod. Nothing can fetch a conversation from the provider after the fact, which is why patchy stores it.
Live streaming needs the status server to read pod logs in the agent namespace, through the API server — it never dials
an agent pod, so the agents' default-deny ingress policy is untouched. Turn it off with
statusServer.liveTranscripts=false (helm) or by dropping the patchy-status-server-agent-logs Role and its binding
(kustomize); completed runs' conversations still serve, since those come from their own ConfigMaps.
Actions and what they really do¶
The action bar renders only the verbs the signed-in user is granted and the finding's state machine allows:
| Action | Available when | What the server writes |
|---|---|---|
| Approve | AwaitingApproval or HandedOff |
spec.approval — the same record the /approve comment writes |
| Retry | Failed |
spec.retry — a recovery request |
| Expedite | any phase up to Queued |
spec.expedite — a standing urgency mark |
| Suspend | any non-terminal phase | spec.suspend: true |
| Resume | a suspended finding | spec.suspend: false |
One action lives on Integrations rather than findings — the configuration view's backfill trigger:
| Action | Available when | What the server writes |
|---|---|---|
| Backfill | a github Integration with code scanning, unsuspended | spec.backfill on the Integration |
The status server never moves a phase. Approving records the approval; the remediation-controller's spawner then drives
AwaitingApproval → Queued (or revives HandedOff → Queued when the approval is newer than the finding's completion)
exactly as it does for a /approve issue comment — the state machine's one-writer-per-edge rule holds no matter which
surface the human used.
Retry recovers a Failed finding to the state immediately before the failure: a failed investigation reverts to
Enhanced (the gate opens the next attempt), a failed remediation — or a pull request closed without merging — re-
queues to Queued (the spawner creates the next attempt). Each edge keeps its single writer: the investigation gate
drives Failed → Enhanced, the remediation spawner Failed → Queued. A retry is consumed by the recovery itself; if
the finding fails again, another retry is required.
Expedite marks the finding urgent for its whole lifetime: the investigation gate skips the accumulation window and
minimum-age wait, and both schedulers rank the finding's runs ahead of all non-expedited work. It does not bypass an
AwaitingApproval hold — that remains approve's job.
The user menu¶
The signed-in name in the top bar drops a menu holding sign-out and two demo actions — Replay deliveries (RBAC verb
replay) and Reset all data (verb reset), both custom verbs on integrations.patchy.bitwisemedia.uk — each
hidden without its grant. Together they re-run the entire pipeline from ingestion; what they write and when to grant
them is covered in Demo tooling.
Access model¶
Two tiers, on purpose:
- Rollups are public. Aggregate statistics carry no finding content; they serve without authentication so the page is useful as a wallboard even with no auth configured.
- Findings require sign-in + RBAC. Without an auth configuration, the findings
views show "sign-in is not configured" and nothing else leaves the cluster. With one, a signed-in user sees findings
only if RBAC grants
getonfindings, and each action button only with the matching custom verb (approve/retry/expedite/suspend/resumeonfindings.patchy.bitwisemedia.uk). The configuration view has its own gate — nativegetonintegrations— and its backfill trigger its own verb (backfillonintegrations.patchy.bitwisemedia.uk, besidereplayandreset). Agent conversations are finding content and ride the sameget findingsgate; the change-notification stream (/events) stays public precisely because it carries no content at all, only the fact that something changed:
rules:
- apiGroups: [patchy.bitwisemedia.uk]
resources: [findings, findingrollups]
verbs: [get, list]
- apiGroups: [patchy.bitwisemedia.uk]
resources: [findings]
verbs: [approve] # this user gets the Approve button, nothing else
Bind roles like this to the values of the OIDC username/groups claims (RoleBindings in the patchy namespace; grants
apply uniformly across its findings). Ready-made viewer / approver / operator / admin tiers ship in
deploy/kustomize/base/rbac.users.example.yaml, and the helm chart renders them with
statusServer.rbac.userRoles=true.
Exposing the page¶
The Service is patchy-status-server:8080. Front it with your Ingress or Gateway on its own hostname — keeping it
apart from the webhook endpoint separates human authn from HMAC-signed machine traffic (helm: statusServer.host +
statusServer.ingress / statusServer.httpRoute). For a quick look without any exposure:
kubectl -n patchy port-forward svc/patchy-status-server 8080:8080
# → http://localhost:8080 (with the dev overlay's `mode: none` auth: full access)
Want the page populated without running the pipeline? The dev overlay ships canned data — one finding per lifecycle phase plus rollup statistics.