Files
pulse/internal/api
rcourtman d167d7f492 Count each finished Patrol investigation once in the weekly digest (#3134)
## Why

The Patrol weekly digest's **Investigated** line, on the Patrol page and
in the weekly email, was unreliable:

- It counted `investigation_outcome` lifecycle rows and read their
outcome from `metadata["outcome"]`. The only writer of those rows
(`updateInvestigationOutcomeLocked`) writes
`prev_outcome`/`next_outcome`, so every counted row was labelled
`unknown`.
- Those rows are later steps of an earlier run's fix (approved, run,
verified), not investigations. Meanwhile the `investigation_updated`
rows every run writes were ignored, apart from one `LastInvestigatedAt`
fallback count per finding.
- So two runs on one finding counted once, a fix approved after last
week's run counted as this week's investigation, and the email could
print "Investigated: 3" followed by "No issues needed a closer look."

## What changes

- `digestInvestigationRuns` counts one investigation per run that
finished in the window, from the rows the findings store already writes
for every run. That includes a run whose own fix was verified before it
closed, and a run whose status a regression cleared mid-run. A fix that
moves on afterwards relabels its run instead of counting again.
- A run that failed with no outcome is labelled `failed`, the bucket
name telemetry already uses. It sits after `timed_out` in
`PatrolDigestOutcomeOrder`.
- The summary-field fallback is removed. Every run since the v6 release
writes lifecycle rows, and the fallback could count one run in two
adjacent weeks.
- Email: `failed` reads "ended with an error". When the count is above
zero but no outcome in the breakdown has wording, the line shows the
count alone instead of "No issues needed a closer look." or the
watch-only sentence.
- `internal/ai/findings.go` is unchanged; the digest reads the rows the
store writes today.

Known limits: lifecycle rows carry no session ID, so a late end written
by an older run is read as the end of whichever run is open. That's the
store-side stale-write problem, which is being closed separately with
session checks. A run whose rows the 50-row lifecycle has already
dropped is not counted.

## Proof

- `internal/ai/patrol_digest_test.go` drives the store's own writers:
the orchestrator's start and end updates, `UpdateInvestigationOutcome`,
`ReconcileInvestigationOutcome`, `RecoverStuckInvestigation`, and
`Resolve` + `Add` for a mid-run regression. The scenario covers ten
finished runs plus one still running. The old code reports 8
investigations, 3 of them `unknown`.
- `internal/api/contract_test.go` follows the scheduled email's path:
store writers → `AISettingsHandler.BuildPatrolDigest` →
`renderPatrolDigestEmail`. The old code reports two runs as `1 unknown`.
- `internal/api/patrol_digest_email_test.go` checks the `failed` wording
and the non-zero-count guard.
- On pulse-dev, `go vet` passes for `./internal/ai/` and
`./internal/api/`, and the full `./internal/ai/` package and every
`PatrolDigest` test in `./internal/api/` pass. The new tests fail
against the previous digest and email (negative controls via
`-overlay`).

## Related

The Patrol page's This week card gets the same `failed` wording and the
same non-zero-count guard in #3132. Until that lands, a week with only
failed runs reads "No issues needed a closer look." on the card. That's
what those runs already showed as `unknown`, so it isn't a regression.
2026-10-23 13:16:09 +01:00
..
2026-10-11 22:20:25 +01:00
2026-10-11 22:20:25 +01:00
2026-10-11 22:20:25 +01:00
2026-10-11 22:20:25 +01:00
2026-10-11 22:20:25 +01:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-08-31 19:47:01 +01:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-04-26 17:26:16 +01:00
…
2026-10-11 22:20:25 +01:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-09-01 01:02:17 +01:00
2026-03-18 16:06:30 +00:00

Internal API Package

This directory contains the API server implementation for Pulse.

Important Note About frontend-modern/

The frontend-modern/ subdirectory that appears here is:

  • AUTO-GENERATED during builds
  • NOT the source code - just a build artifact
  • IN .gitignore - never committed
  • REQUIRED BY GO - The embed directive needs it here

Frontend Development Location

👉 Edit frontend files at: ${PULSE_REPOS_DIR}/pulse/frontend-modern/src/

Why This Structure?

Go's //go:embed directive has limitations:

  1. Cannot use ../ paths to access parent directories
  2. Cannot follow symbolic links
  3. Must embed files within the Go module

This is a known Go limitation and our structure works around it.