mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 13:39:47 +00:00
92f20dafdbece689a67dcf22994ff37d81d39a93
7617 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
92f20dafdb |
Hide Re-investigate while a planned fix is queued
A Patrol investigation can plan a fix and then fail, normally on the final summary call. The run then shows a queued fix with a failed status, and the Investigation section offered Re-investigate because it only looked at the status. Another run would plan a second action for the same problem while the first still waits for approval. The section now hides Re-investigate while the outcome is fix_queued, as it already did for a completed run with a queued fix. The failed run's note uses the muted style of an interrupted run, because the fix awaiting approval is what the operator acts on, not the error. The Pro orchestrator keeps the plan's outcome on such a run and refuses the re-investigation request too. |
||
|
|
a32356ae94 |
Describe the Assistant surface as Pulse Mobile chat
With the web chat window and its backend routes retired (#3126, #3137), the pulse_assistant surface only serves Pulse Mobile's chat. Its manifest description still called it the explanation and handoff surface for Patrol findings, and that text is public: the docs/AI.md overview, the MCP README surface block, and the External agents settings panel all show it. It now names the Pulse Mobile role and says the web chat is retired. The browser's fallback affordances for an Assistant surface with none declared still offered interactive questions. They now match the backend default of tools only, so no client path advertises a question tool nobody can answer. |
||
|
|
070d2a0e53 |
Remove chat backend routes only the web chat used (#3137)
## Why The in-app Assistant chat window was retired in #3126. Several backend routes existed only for that window, and Pulse Mobile, the remaining chat client, never called them. This removes them and the code that only they reached. ## What changes - **Routes removed:** - `GET /api/ai/assistant/surface-tools` - `POST /api/ai/workflow-prompts/render` - `POST /api/ai/question/{id}/answer` - the `/summarize`, `/fork`, `/undo`, `/redo`, `/steer`, `/diff`, `/revert` and `/unrevert` sub-resources under `/api/ai/sessions/{id}`. These now return `404` after the existing `ai:chat` scope check. - **Code removed behind them:** - the handlers and their `AIService` methods; - the chat service's fork, undo, redo, steer, summarize, answer-question and surface-tool methods; - the session store's fork and turn-undo operations and the persisted redo stack (`can_redo` leaves session summaries); - session compaction, the agent loop's steering inbox and question-answer wait, the `steered` message flag, and the `question` and `steer_applied` stream events. - **The question tool is no longer offered.** Interactive chat still offered `pulse_question`, and Pulse Mobile has no way to answer it, so a Mobile turn that called it waited until the request ended. Now: - no profile offers the tool; - the agent loop answers any call with a refusal telling the model to ask in its reply text, the same path Patrol runs already took; - the chat system prompt no longer mentions a clarification tool; - the agent capabilities manifest's Pulse Assistant surface no longer declares the `interactiveQuestions` affordance, so the prompt's affordance text, the fallback governance names, and the generated `docs/AI.md` and `cmd/pulse-mcp/README.md` surface blocks drop it. The External agents settings panel stops showing an "Interactive questions" affordance for Pulse Assistant. The shared `pulse_question` declaration stays. - **Docs and contracts:** - `docs/API.md` lists the remaining routes under "Chat Sessions (Pulse Mobile)", including the rename route Mobile already used. - The ai-runtime and api-contracts rules for the removed workflows are rewritten, and the dependent contracts carry dated notes. ## Unchanged - `/api/ai/chat` and the session list, create, rename, delete, messages and abort routes, which are the Pulse Mobile relay route list. - `/api/ai/status`, which the dev health check uses. - `/api/ai/workflow-prompts/activity`, which Patrol uses. - Patrol investigations, provider routing, model-boundary sanitizing and approvals. - Old session files: they load as before and drop the redo stack and `steered` flags on their next write. ## Not in this PR - The Pulse Assistant surface description in the manifest, which still calls it a handoff surface for Patrol findings. It now describes Pulse Mobile chat and can be reworded with the surface contract when Mobile retires. - The browser's legacy fallback in `frontend-modern/src/api/agentCapabilities.ts`. It still gives Pulse Assistant `interactiveQuestions` when a manifest declares no affordances at all. A same-version server always declares them, so that path is unused; it changes with the surface description. - The dead `highlight.js` chunk rule in `vite.config.ts`. ## Release note > Removed API routes the retired in-app chat window used: Assistant surface tools, workflow-prompt rendering, question answers, and the session summarize, fork, undo, redo, steer, diff, revert and unrevert routes. Pulse Mobile's chat is unaffected. |
||
|
|
dcbfd2d86d |
Remove chat backend routes only the web chat used
The in-app Assistant chat window was retired in #3126. Several backend routes existed only for it, and Pulse Mobile, the remaining chat client, never called them: assistant surface-tools, workflow-prompt render, question answers, and the session summarize, fork, undo, redo, steer, diff, revert and unrevert sub-resources. This removes them along with the handlers, service and session-store methods, the persisted redo stack, session compaction, mid-turn steering and the question-answer wait behind them. The question tool needed more than a route removal. Interactive chat still offered pulse_question, and Pulse Mobile has no way to answer it, so a Mobile turn that called it waited until the request ended. No profile offers it now. The agentic loop answers any call with a refusal that tells the model to ask in its reply text, the path Patrol runs already took, and the chat prompt drops its clarification-tool advice. The capabilities manifest's Assistant surface stops declaring interactive questions, so the generated docs and fallback governance names follow it. /api/ai/chat and the session list, create, rename, delete, messages and abort routes Pulse Mobile uses are unchanged, as is /api/ai/status. Old session files load as before and drop the redo stack and steered flag on their next write. docs/API.md lists the remaining chat routes, and the subsystem contracts record the removal. |
||
|
|
ae19fcf5d6 |
Preserve PVE authentication endings while reading responses
A password login can end while its response is being read: the caller cancels, its deadline passes, or the client's own login timeout expires. Go's HTTP transport can still hand back the server's answer to that abandoned request. The construction fixture answers a departed client with an empty 200, which surfaced as "invalid authentication response" and failed TestClientConstructionHonoursContext and its client-timeout twin intermittently in the required backend check. The shared password exchange now reports the ending instead of diagnosing that answer, keeps transport errors that already carry the ending, and starts no form attempt after an ended JSON refusal. This mirrors the PBS client fix from 18 October. Renewal shares the exchange, so the same rule applies when its own bounded timeout ends a login. Complete sessions are still accepted, and construction still discards a client whose caller has ended. A new control ends the login during response reading, by caller cancellation and by a simulated login deadline, for every unusable answer; without this change all of its cases fail. It also checks that both encoding attempts carry the one login budget the client timeout sets. The TLS fixture now aborts a request whose client has left rather than answering it with an empty 200. |
||
|
|
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. |
||
|
|
3b5d789aeb |
Show unclassified and interrupted Patrol investigation outcomes (#3132)
Since rcourtman/Pulse#3129, Patrol records two more investigation
outcomes:
- `unclassified`: a run that finished without a readable conclusion. It
stays retry-eligible and presents as needs-attention in the loop state.
- `interrupted`: a run that a restart or shutdown cut short. It has no
cooldown and runs again.
The frontend's outcome lists did not know either value, so the findings
store dropped both to "no outcome".
## What changes
- `stores/aiIntelligence.ts` keys the outcomes it accepts by the
`InvestigationOutcome` union in `api/patrol.ts`. `api/ai.ts` now reuses
that union, so the next backend outcome fails type-check instead of
being dropped. The import is type-only because several tests mock
`@/api/patrol` wholesale.
- Every outcome map in `aiFindingPresentation.ts` gains both values:
- `unclassified` reads "No clear conclusion" in the warning tone, joins
the Needs Attention queue and sorts beside `needs_attention`. Its
workflow step asks the operator to read the summary.
- `interrupted` reads "Interrupted — will retry" in the neutral tone and
stays out of the attention queue. Its collapsed row says "Will retry"
instead of "Needs review".
- Neither counts as a failed fix.
- `InvestigationSection` offers Re-investigate after both outcomes. An
interrupted run's note renders as information, not in the error style.
- The This week card names both outcomes with the digest email's wording
and order (`PatrolDigestOutcomeOrder`, `patrol_digest_email.go`).
- Two drawer defects the new labels exposed are fixed:
- The Patrol record showed a red "Failed" run status beside "will
retry". Like the investigation header, it now shows the run status only
until the run has an outcome.
- The record badge used `formatIdentifierLabel` ("Unclassified", "Fix
Executed", "Cannot Fix"). It now uses the shared outcome labels that the
header uses.
- On phones the longer badge broke "Re-investigate" across two lines.
The header row now wraps.
Telemetry is unchanged: the two outcomes already map to `in_progress`
and `other`.
A second commit lines the This week card up with the digest-count
backend change:
- The card names the digest's new `failed` bucket ("ended with an
error"), for a run that ended on an error with no outcome. It sits right
after timeouts, as in `PatrolDigestOutcomeOrder` and the email.
- When none of a week's investigations has a worded outcome, the
Investigated tile shows the count alone. It used to say "No issues
needed a closer look" under a non-zero count. The email already does
this.
- That commit has its own receipt (`196f770aad8a3198.json`) from a pass
over live and fulfilled digest responses at 1440 and 390.
## Before and after
Private Pulse Pro stack: pulse-enterprise#139 built against this tree on
`ec40662af4`, with a mock estate and seeded findings, investigation
sessions and a week of runs.
| Surface | main | this branch |
| --- | --- | --- |
| This week, Investigated tile | 5, "No issues needed a closer look." |
5, "3 finished without a clear conclusion, 2 interrupted by a restart."
|
| Needs Attention filter | 1 | 4 (three unclassified, one
needs_attention) |
| Interrupted finding row | Needs review | Will retry |
| Unclassified drawer | raw `unclassified`, no Re-investigate, green
"Completed" record badge | "No clear conclusion", Re-investigate |
| Interrupted drawer | raw `interrupted`, red "Failed" record badge, red
restart note | "Interrupted — will retry" (neutral), muted restart note
|
| 390px drawer header | Re-investigate splits over two lines | the row
wraps; the button stays whole |
## Verification
- Vitest on pulse-dev:
- The 9 touched test files pass, 447/447.
- The wider api/stores/utils/AI/patrol/pages/shared run passed 8,858
tests. The one failure was `agentInstallCommand.test.ts`, caused by the
script's umask; it passes 90/90 with the default umask.
- Negative control: 26 of the new assertions fail against main's
sources.
- The Frontend CI job was replayed on pulse-dev:
- Format check, headers, cpd, type-check, build and bundle size passed.
- The lint chain caught a semicolon in the new "Read summary" copy.
After the fix every audit passed.
- The full suite passed 22,156 tests. Three docs-reading files timed out
at 5 s under load (`shippedDocFragments`, `operationalUpgradeHelp`,
`externalAgentGuide`); they pass alone, 8/8.
- Browser receipt
`frontend-modern/browser-verification/f1f2d9ca0f32c251.json`: headless
Chromium at 1440 (dark and light) and 390 on the final bytes, exercising
the This week card, the finding list and the review drawers of an
unclassified, an interrupted and a needs_attention finding.
- Governance: the per-commit canonical completion guard (with the
Contract-Neutral trailer), the receipt range check, and the contract and
registry audits all pass.
The presentation is documented in `patrol-intelligence.md`. ai-runtime
and api-contracts already describe both outcomes from #3129, so the
commit carries a Contract-Neutral trailer for them and for
frontend-primitives.
|
||
|
|
b633e54105 |
Count each finished Patrol investigation once in the weekly digest
The weekly digest counted investigation_outcome lifecycle rows and read their outcome from a metadata["outcome"] key the findings store never writes, so every one of them showed as "unknown". Those rows are later steps of an earlier run's fix (approved, run or verified), not new investigations, while the investigation_updated rows every run writes were ignored apart from one fallback count per finding. 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 "No issues needed a closer look." under a non-zero count. The digest now counts one investigation per run that finished in the window, from the rows the store already writes for every run, including a run whose own fix was verified before it closed. A fix that moves on afterwards relabels its run instead of counting again. A run that failed with no outcome is "failed", the bucket telemetry already uses; the email words it "ended with an error" and no longer denies a non-zero count. 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. The tests drive the findings store's own writers rather than hand-built rows, which is how the wrong metadata key went unnoticed. |
||
|
|
a408736bc4 |
Keep stale investigation runs from overwriting newer finding state
The private investigation orchestrator writes a finding's investigation fields through orchestratorFindingsAdapter, and every write was applied unconditionally. A run still in flight when its finding was resolved and detected again, or when the stuck-run sweep gave it up and a retry started another run, overwrote the newer state with its own session, status, outcome and attempts when it finished. A stale "resolved" made the regressed occurrence terminal, so Patrol stopped investigating it. Closing writes now go through FindingsStore.CompleteInvestigation, which records them only while the finding still names the run: the session must be the finding's current one and the run must not predate the finding's latest regression. Admission goes through StartInvestigation, which refuses a run whose session started before that regression, so a delayed admission cannot install an earlier occurrence's attempt count or replace a newer run. Both compare a run's start only with an occurrence start this process observed: a regression, or the finding's removal by ClearAll or Cleanup, kept unpersisted with the clock's monotonic reading for a day. A regression an earlier process recorded predates every run of this one, so a clock set back across a restart cannot make every new run look stale. Records and action outcomes keep comparing with LastRegressionAt, since they also judge sessions of earlier processes. The adapter takes each run's start from the orchestrator's session store, so the aicontracts boundary and the enterprise orchestrator are unchanged. |
||
|
|
04a49ed72a |
Name failed digest runs and keep the Investigated tile honest
The digest's investigation counts gain a "failed" bucket for a run that ended on an error with no outcome, the name telemetry already uses. The digest email words it "ended with an error" and places it right after timeouts. The This week card now does the same. When none of a week's investigations had an outcome the card has wording for, the Investigated tile still said "No issues needed a closer look" (or the watch-only sentence) under a non-zero count. Like the email, the tile now shows the count alone and keeps those sentences for a week with no investigations. |
||
|
|
5c7d2c6e67 |
Show unclassified and interrupted Patrol investigation outcomes
Since rcourtman/Pulse#3129 Patrol records two more investigation outcomes: unclassified, for a run that finished without a readable conclusion, and interrupted, for a run that a restart or shutdown cut short. The frontend's outcome lists did not know either value, so the findings store dropped both to no outcome. An unclassified run never reached Needs Attention and offered no Re-investigate, an interrupted row asked the operator to review it, the drawer showed the raw keys, and the This week card said "No issues needed a closer look" under five investigations. The store now keys the outcomes it accepts by the InvestigationOutcome union in api/patrol.ts, which api/ai.ts reuses, so the next outcome fails type-check instead of vanishing. Every presentation map gains both values. Unclassified reads "No clear conclusion" in the warning tone and joins the attention queue. Interrupted reads "Interrupted — will retry" in the neutral tone, its row says "Will retry", and its run note is no longer styled as an error. The This week card uses the digest email's wording and order. A browser pass on a Pro stack found two drawer defects the new labels exposed. The Patrol record showed a red Failed run status beside "will retry", and on phones the longer badge broke Re-investigate across two lines. Like the investigation header, the record now shows its run status only until the run has an outcome, its badge uses the shared outcome labels, and the header row wraps. Contract-Neutral: ai-runtime and api-contracts already record the unclassified and interrupted outcomes (#3129); the API clients only mirror them and the record badge reuses the shared outcome label. The Patrol presentation is documented in patrol-intelligence.md. |
||
|
|
85fb199ff5 |
Merge main into feat/patrol-typed-conclusions
Main gained the Assistant retirement (#3126), which also appended notes to the agent-lifecycle, ai-runtime, api-contracts and storage-recovery contracts. Keep both: main's notes, then this change's. |
||
|
|
b3b5ad6e1f |
Record Patrol investigation conclusions as a typed tool call
Patrol investigations inferred their outcome from prose markers, so a bold or bulleted RESOLVED line became the terminal needs_attention and the finding was never investigated again. Timeouts clustered on subscription agents. Patrol's 60s stream idle timeout bounds a whole buffered CLI turn there, three investigations queued on one serial CLI slot, and the final summary call was capped at 20s. - Add patrol_conclude_investigation, an investigation-only, side-effect-free capture of resolved, cannot_fix or needs_attention. It is carried on InvestigationRunResult and OrchestratorInvestigationResult. - After a conclusion, the next turn is the summary. The final turn offers only the tools that end a run. A deadline reserve makes the current turn final before the run times out, and the summary call may use the time left. - Give investigation turns on subscription agents a 4-minute turn budget, a 25-minute default timeout and one investigation at a time. These limits are republished on config reload without holding the service lock across the Patrol lock. - Derive Patrol's outer deadline from the effective timeout. Sessions now carry their own deadline, and the stuck-run sweep measures a live run against it, so neither a lowered timeout nor the previous run's time resets a running investigation. - Record a run orphaned by a restart as interrupted, not timed out. A first investigation cut short that way used to stay running for good, which also blocked a manual re-run. Recovery applies only while the finding still shows that run, takes a result the run's session already recorded, and keeps an action outcome such as fix_queued. - Add unclassified and interrupted outcomes, with lifecycle, telemetry bucket, digest and record handling. |
||
|
|
f330c76481 |
Merge the dompurify and vitest bump into the Assistant retirement
Both branches edit the frontend manifest, lockfile and dependency floor list: this branch drops highlight.js, the bump raises dompurify and vitest past the 22 October advisories that fail the required audit. Each conflict keeps the bump's versions without highlight.js. |
||
|
|
2d0232ce69 |
Bump dompurify and vitest past the 22 October advisories
npm audit started failing the required Frontend check on every branch once GHSA-cg2g-xh64-v4q7 (critical, SAFE_FOR_TEMPLATES bypass), GHSA-mrv2-3pqr-j5jr and GHSA-c6gr-qvv4-5v3f were published against dompurify <=3.4.16, and GHSA-qxg6-89mg-rqv8 and GHSA-xp26-3454-mjgp against @vitest/mocker <4.1.12. dompurify moves to 3.4.17, vitest and @vitest/coverage-v8 to 4.1.12, and the dependency security test raises its floors to match. The deployment-installability contract records the reviewed floors, as the 30 September DOMPurify bump did. The lockfile takes only what those updates resolve: the vitest family, the chai, obug, std-env and tinyrainbow versions its ranges now pick, and one deduplicated picomatch 4.0.7 in place of the nested copies. The bundled wasm entries newer npm writes for @tailwindcss/oxide-wasm32-wasi are left out, as they are on main. Dependabot's group PR 3058 carries these bumps among others but has been blocked on unrelated failures since 21 October. |
||
|
|
cd514df7f9 |
Keep record drawer headings whole on phones
A record drawer's owner link repeated the object's name next to the
heading that already shows it. On a phone the button took about half
the header, so a long heading such as a Kubernetes pod's broke mid-word
across four lines ("paymen / ts-worker- / 58484d44f7 / -f6gzh").
The link now reads "Open" below the sm breakpoint and "Open <name>"
above it, keeping the full label as its accessible name and tooltip.
On a 390px phone the button drops from 192px to 70px and the pod
heading wraps once, at a hyphen. Desktop is unchanged.
|
||
|
|
28172a907b |
Retire the Assistant chat window; alert Investigate runs Patrol
Pulse Assistant was a chat drawer beside the monitoring UI. Operators who want to talk to their infrastructure already use Claude Code, Codex, or another MCP client, and Pulse MCP gives those agents the same governed tools. Keeping a second, weaker chat in the web app cost a 66 kB chunk, a launcher on every page, hand-off buttons on alerts, findings, approvals and drawers, and thousands of lines of OpenCode-parity drawer code, for a feature the maintainer judged ineffective. This removes the browser chat and everything that only fed it: - The drawer, its launcher (desktop and phone), the command-palette Assistant commands, the Escape and blocking-dialog shell plumbing, and the chat store, client, dev stream fixture and generated SSE types. - Every "Open in Assistant", "Explain with Assistant", "Discuss" and "Ask Assistant" hand-off on alerts, incident timelines, Patrol findings, approvals, run history, the attention workbench and resource drawers, with their context models and the chat-only helpers they used. - The Assistant settings page and its session maintenance section. The page becomes "External agents" at /settings/pulse-intelligence/agents; the old /assistant path, ?tab=assistant and system-ai-assistant redirect there with the hash kept. Alert Investigate now starts a targeted Patrol run (resource, alert ID and type) and says where the findings land, and it ignores repeat clicks while the request is in flight. The run route needs an admin with ai:execute, so security status gains settingsCapabilities.patrolRun, derived from the same snapshot as the other admin capabilities but following the route for org-scoped admins, organization-bound tokens and a cookie beside a token, and Investigate is offered only where it is true. The Patrol tool transcript block moves to components/patrol. A small aiAvailability store holds the session assistantEnabled fact that gates Patrol entry points and Investigate; it now clears and refreshes on organization switch, refreshes when organization loading settles on another org and after provider removal, and ignores answers that arrive after a newer one. The backend chat service stays: Patrol investigations run on it and Pulse Mobile chat uses it until 31 March 2027, so /api/ai/chat, the session endpoints and the stream contract are unchanged. A new contract snapshot pins the steer_applied event that only the deleted TypeScript projection used to cover. highlight.js, used only by the chat's code highlighter, leaves the dependencies, as does the DOMPurify carve-out that kept its fence hints. Contracts, registry and readiness proofs drop the retired surfaces; the ai-runtime contract loses about 1,600 lines of drawer rules while keeping every backend session, stream and provider rule. The production build ships about 100 kB less gzipped JavaScript. |
||
|
|
3745f4738d |
Let Docker and vSphere host rows show their workloads
On the Proxmox overview a node row narrows the guest table to that node and the chevron opens the node's drawer. Docker and vSphere host rows opened the drawer instead, so seeing one host's containers or VMs meant scrolling past every other host's group or finding the Host filter. Docker and vSphere host rows now work like Proxmox nodes: a click narrows the containers or VMs to that host, titles the table "...on <host>" with Show all hosts, marks the row and moves focus to the heading; a second click clears it. The chevron opens the drawer and stays visible on phones, where the host column takes the width Uptime did not need so names stay readable. Docker scopes through the ?host= filter its containers table already read; every host now stays listed and the scoped one is marked, where that filter used to hide the others (the Docker link from a VM drawer lands the same way). A vSphere host scopes to the scope the VM table already gave its own VMs, matched within its vCenter connection, so host and VM rows cannot disagree. The name button, scoped heading and focus handling move into shared PlatformHostScope pieces that the Proxmox overview now uses too. The Docker Swarm tab's host table has nothing under it to scope and keeps opening the drawer on a row click. |
||
|
|
177325cb03 |
Let Coverage restore evidence details wrap
The Coverage drawer's restore evidence ran long snapshot descriptions
("checkout-web-02 local rollback snapshot") straight past the drawer
edge with no tooltip. The cells asked for whitespace-normal, but the
global `.table-fixed td` rule forces nowrap and outranks a utility
class on the cell, so the request did nothing.
Put the wrap on each value's span instead, the way the shared drawer
detail tables already do, and pin it in the Coverage test.
|
||
|
|
30024b0f85 |
Wrap drawer sentences and open fleet details under the tile
Two leftovers from the drawer consistency pass.
TrueNAS pool and disk drawers cut their summary sentences off with an
ellipsis ("ZFS pool archive is DEGRADED and ..."), so the one line that
explains the pool's state was the one a user could not read. Those
rows, and vSphere's hardware upgrade and Tools install errors, now
wrap; short values stay on one line.
The availability fleet view opened its drawer below the whole grid, so
clicking a tile near the top of a large fleet showed nothing nearby.
The drawer is now a full-width grid item right after the tile it
belongs to, so it opens beneath that tile's row. The fleet list is
reconciled by id so live updates refresh the open drawer instead of
remounting it, and closing it returns focus to the tile.
|
||
|
|
de2c9729c2 |
Let TrueNAS record drawers open the object they are about (#3119)
Record drawers on the other platforms link to their owner ("Open sdc")
wherever the owner's page can narrow to it. TrueNAS tabs only read a
storage kind filter, so its alert and protection drawers had no link:
a link that lands on a whole tab leaves the user hunting for the row.
TrueNAS tabs now keep their table search in the URL (?q=), the way the
Kubernetes and Machines pages do. The page-path helper emits it for
TrueNAS pools, datasets, disks, apps, VMs and shares (not for a disk
named only by an alert, which may not match a row), so the alert and
protection drawers gain the owner link and land on the tab already
narrowed, with the filter visible in the search box. Systems keep an
unfiltered link-free Overview because that table has no search box.
Also:
- The Coverage drawer's restore evidence wraps long snapshot and
archive names instead of cutting them; table rows still use one line.
- The disk-live-refresh browser proof waited for the full "Replace Now"
verdict on the phone, where the health column shows "Replace"; it now
waits for the label the layout shows, and passes on phone WebKit.
|
||
|
|
3704f006f3 |
Let TrueNAS record drawers open the object they are about
Record drawers on the other platforms link to their owner ("Open sdc")
wherever the owner's page can narrow to it. TrueNAS tabs only read a
storage kind filter, so its alert and protection drawers had no link:
a link that lands on a whole tab leaves the user hunting for the row.
TrueNAS tabs now keep their table search in the URL (?q=), the way the
Kubernetes and Machines pages do. The page-path helper emits it for
TrueNAS pools, datasets, disks, apps, VMs and shares (not for a disk
named only by an alert, which may not match a row), so the alert and
protection drawers gain the owner link and land on the tab already
narrowed, with the filter visible in the search box. Systems keep an
unfiltered link-free Overview because that table has no search box.
Also:
- The Coverage drawer's restore evidence wraps long snapshot and
archive names instead of cutting them; table rows still use one line.
- The disk-live-refresh browser proof waited for the full "Replace Now"
verdict on the phone, where the health column shows "Replace"; it now
waits for the label the layout shows, and passes on phone WebKit.
|
||
|
|
7737469045 |
Release v6.6.0-rc.6 (#3115)
Release preparation for `v6.6.0-rc.6`. After this merges, the release workflow runs at this exact head `63d35086b19ebd76043ddbad3e246d42826c1d66`. |
||
|
|
4aa3d7aa5f |
Finish moving the last bespoke drawers onto the shared shape
The earlier drawer changes left a few surfaces that still looked or behaved differently, plus code nothing used any more: - Proxmox Mail Gateway had two drawers for one gateway; the row now opens the shared resource drawer with mail flow as its Mail tab. The Ceph cluster drawer gains the canonical header, Overview (alerts, Worth checking, capacity history, cards, closed Details) and Manage, and its dot follows the row instead of reading grey on missing health. - The Kubernetes cluster drawer's Deployments tab repeated the Workloads table with dead-end rows; it is gone, and Namespaces opens the Workloads tab scoped to the cluster and namespace with one action. The Docker Swarm drawer tab could not be reached from any page; gone. - On phones, windowed tables (Kubernetes deployments, pods) and the Storage tab's grouped pools stopped at 70-90% of the width: a colspan=100 spacer or colspan=99 group row created phantom columns that took the width hidden columns left. Those rows now span exactly the visible header cells. - The node and Docker host drawers gain the guest drawer's Open action; the Docker host withholds readings while offline or stale, lists operator state first in Manage and shows its action history. A guest's physical disks open the same disk Overview as the Storage tab, with the guest agent's disk temperature alerts. - One "No active alerts" line, one timestamp row and wrapping alert subjects replace per-drawer copies; PBS and PMG alerts match on their instance id so drawers find them without a metrics target. - Removes the old disk detail and its presentation code, the Swarm and Kubernetes Deployments drawers and their presentation helpers, and drawer state nothing read. |
||
|
|
63d35086b1 |
Prepare v6.6.0-rc.6
Release candidate cut from main at
|
||
|
|
750292e21d |
Build every record drawer on the shared record panel
Alerts, events, tasks, services and backup rows open a "record" drawer: something that happened to an object, not an object to manage. Many of them were hand-built grids that looked like a different product: no heading, no status, no way to close from inside, raw timestamps, labels cut to three, and no link to the object they were about. - The five Docker Swarm tables, Docker networks and storage usage, Proxmox replication and coverage now open the shared record panel: the subject's name and the row's own status dot, a one-line summary, wrapping sections with human dates, and an "Open <name>" link where the app can focus the owning object (and none where it would only land on a whole tab). - Recoverable restore points had no drawer, and on phones lost their location entirely; they now open one. - Kubernetes events, ConfigMaps, Secrets and RBAC objects opened the full object drawer, which never showed an event's message and offered maintenance windows on it. They open record drawers instead; a Secret's key names and values are never shown. - Coverage allowed several rows open at once; it now opens one, like every other table. - Agent Doctor targets open one at a time with a proper header, shared notices and a closed Details for connection evidence. - Availability fleet tiles behave as disclosures and open the same table-row drawer as the table view. |
||
|
|
a98b5a69dd |
Bring storage and platform drawer content up to the guest drawer
The Proxmox guest and node drawers lead with what is wrong, chart the last day and keep the long tail in one closed Details. Other drawers did not: - Storage pool and physical-disk details on the Proxmox Storage tab had no heading, status dot or collapse control, no alerts line or Worth checking, a pool chart defaulting to 7 days with its own picker, a disk "History" and "Live I/O" section, and every config, topology and SMART table open by default. Both are now object drawers in the shared detail row, with Overview (alerts, Worth checking, 24-hour charts, facts, ZFS/SMART cards, closed Details) and Manage (operator state). A disk alert that already states the verdict is not repeated beneath it, and worn flash is stated once by the verdict. - TrueNAS ZFS errors and disk risk, vSphere datastores that are not accessible, Kubernetes containers that are not ready and cordoned nodes sat two or three closed disclosures deep and never reached Worth checking. Platform sections now join the same split as every other drawer: warnings lift, cards show, the rest waits in Details. - Kubernetes Deployments, autoscalers, Services, Ingresses and volumes had no section at all, so columns hidden on phones were unreachable. Docker image and volume drawers showed nothing about the image or volume. No field is dropped; it moves to where the guest drawer keeps it. |
||
|
|
5d6d9e2dcf |
Give every drawer the Proxmox drawer's heading and shell
The Proxmox guest and node drawers were reworked into a clear shape, but the drawers on other pages kept older pieces that disagree with them: - The generic resource drawer coloured its status dot from the raw status while the row above it used each platform's own verdict, so a succeeded pod read red, a stale machine read Online and a running but failing container read green in the drawer while the row said otherwise. Tables now pass the indicator behind their row dot, and the drawer repeats it. - Record drawers (alerts, activity, services, protection jobs) were headed by a generic label such as "Alert detail", had no status dot, named their collapse button after an internal id, and cut alert sentences and identifiers with an ellipsis that phones cannot hover. They are now headed by their subject with its status and a one-line summary, take an actions slot for owner links, and wrap their values. - Five tables (Ceph, Mail Gateway, Replication, Coverage, Docker Networks) replaced the detail row's content class to change padding and silently dropped its phone width cap. The shell is no longer overridable, and a guard keeps it that way. - DrawerSubjectHeading is now the one subject heading: names wrap rather than truncate, and the generic drawer uses it with its source badges beside the name, an Open action for a saved web URL, and no duplicate health chip (the issue already leads Worth checking). Verified on a full mock estate at 1440 and 390 against origin/main: every drawer dot now matches its row and no route logs a console error. |
||
|
|
a5b8917331 |
fix(mock): re-sight shared storage and Ceph on the PVE cadence (#3110)
Stacked on #3109 (`fix/mock-pve-guest-sighting-cadence`); once that merges, this PR's diff reduces to its own commit. ## Problem In mock mode, the update loop picks PVE inventory by node name, one cohort at a time. Shared storage belongs to no single node: the PBS stores use the owner `"cluster"`, and `shared-storage`, `ceph-rbd` and `cephfs-data` use `"shared"`. The mock Ceph cluster has no node either. None of them ever matched a cohort, so the loop never re-sighted them. They kept their startup stamp and turned into stale Proxmox sources within minutes, at every `PULSE_MOCK_UPDATE_INTERVAL`, the default 2s included. The Ceph cluster was worse. `generateCephClusters` set its `LastUpdated` from the wall clock and nothing touched it again, not even the full `UpdateMetrics` refresh. Both readings show on the public demo's Storage and Ceph views. ## Fix Cluster-scoped inventory is sighted on the first cohort of each PVE rotation. That is the cadence `mock.ProxmoxRefreshInterval` reports (#3109), and the mock Proxmox stale threshold derives from it. A row is sighted only while one of its reporting nodes is online; when a row lists no reporting nodes, any online node of its instance counts. The same selection also resamples shared-storage usage, which the cohort loop skipped as well. The full refresh path keeps sighting everything. **Mock-only.** The real storage poll stamps cluster storage with the poll's start time on every poll (`monitor_polling_storage.go`, `LastSeen: startTime.UTC()` for the `Node: "cluster"` rows). `pollCephCluster` runs after every storage refresh and sets `LastUpdated: time.Now()` (`ceph.go`). ## Proof **Regression test:** `TestFixtureGraphSlowTicksKeepProxmoxClusterSightingsFresh` drives the loop's own cohort sequence through two rotations at 2s, 7s, 15s and 60s ticks. It records each shared store's and the Ceph cluster's worst sighting age just before a tick. It fails on #3109's head: at 15s ticks every shared store reaches 5m0s and Ceph ~1h43m, against a 30s bound. It passes with this change. The rest of the cohort and cadence tests in `internal/mock` still pass, and the full `internal/mock` suite passes. **Live before/after on pulse-dev.** Two builds ran side by side: #3109's head (before) and this branch (after). Both used fresh data dirs and the demo env (`PULSE_MOCK_MODE=true DEMO_MODE=true PULSE_MOCK_NODES=8 ... PULSE_MOCK_UPDATE_INTERVAL=15s PULSE_MOCK_SEED_METRICS_STORE=true PULSE_TELEMETRY=false`). Storage and Ceph resources from `/api/resources` were tallied once a minute for 10 minutes: | t | before: storage/ceph with a stale Proxmox source | before: oldest sighting | after: stale | after: oldest sighting | |---|---|---|---|---| | +0m | 3 (Ceph, ceph-rbd, cephfs-data) | 167s | 0 | 20s | | +1m | 8 | 227s | 0 | 20s | | +2m | 8 | 287s | 0 | 20s | | +5m | 8 | 467s | 0 | 5s | | +10m | 8 | 767s | 0 | 20s | On the before build, the 8 stale sources were the Ceph cluster, two `ceph-rbd` and two `cephfs-data` rows, `shared-backup-fabric`, `west-backup-primary` and `west-backup-offsite`. From t+1m onwards, the before build showed `storage/warning/proxmox:stale=7` and `ceph/warning/proxmox:stale=1`. The after build stayed at `storage/online/proxmox:online=22` and `ceph/online/proxmox:online=1` all through; its one Proxmox storage in `warning` has an online source. |
||
|
|
a4971e9873 |
fix(mock): keep PVE guest sightings fresh on slow mock ticks (#3109)
## Why A mock stack with the public demo's settings (`PULSE_MOCK_UPDATE_INTERVAL=15s`, 8 nodes, 6 VMs + 4 LXCs per node) drifts healthy running guests into Attention within minutes: `status: "warning"`, `health.reasons: telemetry_stale`, `sourceStatus.proxmox.status: "stale"`. Root cause: the mock update loop sights PVE nodes and their guests through ten fixed cohorts, sized for the default two-second tick (every node within 20s). The count never followed the tick interval, so at 15s each node's guests were sighted only every 150s, while the registry judges Proxmox sightings stale after 60s. ## What - `internal/mock`: the PVE estate rotates through the largest divisor of the ten cohorts that still sights every node within 30s (`proxmoxCohortCount`). At the default 2s tick nothing changes (10 cohorts). At 15s it uses 2 cohorts, and from 16s up every node is sighted on every tick. A divisor keeps the loop's cohort index mapping each node to exactly one tick per rotation. Per-second churn never exceeds the 2s default. - `internal/monitoring`: when a single tick is slower than that budget, the mock Proxmox stale threshold is derived from the actual sighting cadence (`mock.ProxmoxRefreshInterval`), never below the configured PVE derivation. This mirrors how |
||
|
|
63ae0eb6f8 |
fix(mock): re-sight shared storage and Ceph on the PVE cadence
The mock update loop selects PVE inventory by node name, one cohort at a
time. Shared storage merged under a cluster-level owner ("cluster" for
the PBS stores, "shared" for shared-storage, ceph-rbd and cephfs-data)
and the mock Ceph cluster match no node, so the cohort loop never
re-sighted them. They kept their startup stamp and read as stale Proxmox
sources within minutes of start, on every tick rate. The Ceph cluster
was worse: its LastUpdated was set from the wall clock at generation and
never touched again, even by the full refresh path. The public demo's
Storage and Ceph views show this.
Sight cluster-scoped inventory once per PVE cohort rotation, the cadence
mock.ProxmoxRefreshInterval reports and the mock Proxmox stale threshold
derives from, while any of its reporting nodes (or any node of its
instance when none are listed) is online. The same selection now
resamples shared storage usage, which the cohort loop also skipped.
Mock-only: the real storage poll stamps cluster storage with the poll's
start time on every poll, and pollCephCluster sets LastUpdated each
poll.
|
||
|
|
f69860b153 |
fix(mock): keep PVE guest sightings fresh on slow mock ticks
The mock update loop sights PVE nodes and their guests through ten fixed
cohorts, sized for the default two-second tick (every node within 20s).
The count did not follow PULSE_MOCK_UPDATE_INTERVAL, so the public demo's
15s tick sighted each node's guests only every 150s while the registry
judges Proxmox sightings stale after 60s. Within minutes most running
guests read as warning/telemetry_stale with proxmox source "stale", and
the Proxmox overview's Attention chip climbed to 64 of 77 guests.
Rotate the PVE estate through the largest divisor of the cohort count
that still sights every node within 30s, down to every node per tick. A
divisor keeps the loop's cohort index mapping each node to one tick per
rotation, and per-second churn never exceeds the two-second default. For
ticks slower than that budget, derive the mock Proxmox stale threshold
from the actual sighting cadence (mock.ProxmoxRefreshInterval), never
below the configured PVE derivation, the same way
|
||
|
|
46a87e1a46 |
Point localized READMEs at the current licence screen
The German and Spanish getting-started guides told paid customers to
activate under Settings -> Plans -> Existing purchases. That heading
left the app in June (
|
||
|
|
4809e1085f |
Mute the workload history hover hint
The one-time "Hover a guest to preview history" tip in the guest toolbar had no colour class, so it rendered in body-text white while every control around it (View, Add filter, the History label) is muted slate. A passive tip outshone the controls and read as a stray label. It now uses the muted token, so it reads as a caption for the History range control it explains. Reveal, clear-on-hover and phone hiding are unchanged. The README workspace capture and social preview are refreshed from the same build so they show the muted hint. Contract-Neutral: Hint colour token only; no change to what the workloads toolbar shows or when |
||
|
|
5a1c31bdc6 |
docs: refresh README product screenshots
The README and SCREENSHOTS captures dated from 6 August. Since then the Proxmox overview moved the node table above the guest filters and gained the history range picker, and the Patrol page was rebuilt around a decision inbox with Protection and Activity tabs, so the Patrol image showed a page that no longer exists. The new captures come from a main build running the public demo's mock estate, at 2x for high-density screens, and the social preview reuses the new workspace capture. SCREENSHOTS.md no longer gives demo credentials because the demo signs visitors in automatically. |
||
|
|
6cc795a27c |
Prepare v6.6.0-rc.6
Release candidate cut from main at
|
||
|
|
045e7af651 |
alerts: keep only the newest evidence envelope in history entries (#3096)
## Why After #3093, the remaining weight of the alert history fold is evidence nothing reads. On the long-running dogfood install, a resolved snapshot averages 21 KB, and 83% of that is `evidence`: a trail of up to 64 envelopes (35 on average), across 1,895 occurrences kept for 90 days (#1803). Nothing in Pulse reads `Alert.evidence` from history. The incident panel's evidence comes from the incident timeline endpoint, the AI's alert history uses recently resolved alerts, and the attention summary and quality telemetry never touch it. ## What changed - The fold trims each decoded snapshot to its newest envelope, by `observedAt` and then position. An updated envelope keeps its slot, so the last one isn't always the newest. Merging takes the incoming snapshot's evidence, so the fold never holds a full trail. - History output applies the same rule to live overlays and the JSON fallback, so every `GET /api/alerts/history` entry carries at most one envelope. - `operationalRecord.evidenceIds` and `transitions[].evidenceIds` keep their full lists. The event log keeps every snapshot whole, and `GET /api/alerts/active` still returns an open alert's full evidence. - The api-contracts history clause, `docs/API.md` (and its shipped mirror) and the alerts contract say so. This is an intentional change to a public API response, approved by the maintainer. A client that read full evidence from history should read the active alert, the incident timeline or the event log. ## Measured On a log shaped like the dogfood install's (1,900 occurrences, 35 envelopes each), the history fold retains 2.1 MB of heap, down from 27.2 MB. ## Checks (pulse-dev) - New tests: - newest by time and tie-breaking; - the fold holds one envelope while the record keeps all 64 IDs; - the event log stays whole; - live overlay and JSON fallback; - direct output shaping, with its input untouched; - `docs/API.md` states the rule. - Negative controls: keeping full evidence in the fold, in the output, or choosing by position each fail their test. - `internal/alerts/...`, `internal/api/alerting/...` and `docsLinks.test.ts` pass. Prettier, vet, the completion, contract, docs-mirror and browser-verification guards all pass. Related to #1803 |
||
|
|
e19c97c6cb |
agents: name command access plainly and ask for it before the mint (#3094)
## Problem Docker and Podman container updates need an agent with command access. The only way to grant it was a checkbox called **Enable legacy combined command profile**. It sat behind **Show advanced connection and install options**, which only appears after the install token is minted. The word "legacy" told the users who want updates not to use it. In #1728 a user missed it entirely and hand-edited token scopes instead. ## Is the option being retired for container updates? Not yet, so it should not be labelled as if it were. - `docker_container_action_executor.go` sends updates through the agent command session. - The separate action runner implements `container.update`, but `AGENT_SECURITY.md` lists its container actions as unqualified. - The runner needs the safe collector profile. Under that profile, rootful Docker gets summary inventory only, with no update checks or actions. The security posture is unchanged. Command access is off by default. Choosing it mints an execution-scoped token and records `command-capable`. A `monitoring-only` install still cannot be promoted remotely. ## Changes - The checkbox now reads **Allow Pulse to run commands on this host** and sits in the token step, so the choice is made before the token is minted. Its description says it is needed for container updates, start, stop and restart, and Patrol actions. It also says Pulse can then run commands as root, and that the option cannot be switched on later from Pulse. - The token summary no longer claims command actions can be enabled per host from Settings. It now states the real choice. - One shared label (`getUnifiedAgentCommandAccessOptionLabel`) feeds the installer, the locked Docker update pill and the Proxmox setup guide, so their wording cannot drift apart. - `DOCKER.md`, `AGENT_SECURITY.md` and `PRODUCTION_SECURITY.md` name the option the same way. ## Verification - Private mock stack, 1400 px and 375 px: - The option appears in step 1 before a token exists. - Ticking it and then generating a token mints with command execution, and every install command carries `--enable-commands`. - Unticking it regenerates the token without that permission. - The advanced options no longer hold it. - The Docker and Proxmox notes name it. - Mock Docker hosts carry no agent facet, so the "installed without command access" pill cannot render on mock data. Its copy is covered by the `containerUpdateBadgeModel` and `ContainerUpdateBadge` tests. - Settings, shared components, Docker, the container drawer and utils suites pass: 4436 tests on a rerun. The first run on a heavily loaded pulse-dev had one unrelated failure, and the rerun passed in full. Typecheck, ESLint and Prettier are clean. - Browser receipt included. Contract notes added for agent-lifecycle, api-contracts and frontend-primitives. |
||
|
|
9bb421f230 |
alerts: keep only the newest evidence envelope in history entries
Most of the alert history fold was evidence nothing reads. On the dogfood install a resolved snapshot averaged 21 KB, 83% of it the evidence trail of up to 64 envelopes (35 on average), across 1,895 occurrences kept for 90 days (#1803). No Pulse UI, AI tool or API handler reads Alert.evidence from history: the incident panel uses the incident timeline, and the AI uses recently resolved alerts. The fold now trims each decoded snapshot to its newest envelope, by observedAt and then position (an updated envelope keeps its slot, so the last one is not always the newest). History output applies the same rule to live overlays and the JSON fallback, so every /api/alerts/history entry carries at most one envelope. operationalRecord.evidenceIds and transitions keep their full lists, the event log keeps every snapshot whole, and /api/alerts/active still returns an open alert's full evidence. On a log shaped like the dogfood install's, the history fold went from 27.2 MB to 2.1 MB of retained heap. Negative controls: keeping full evidence in the fold, in the output, or choosing the newest by position each fail their test. The alerts tree passes on pulse-dev. Related to #1803 |
||
|
|
487ab8a7c4 |
alerts: answer history reads from the shared fold without full copies (#3093)
## Why A heap profile of the long-running dogfood install (77.6 MB live after a forced GC) shows the alert history projection holding 36 MB (47%). It keeps one merged alert per occurrence for the event log's 90-day retention: 1,895 occurrences there, with resolved snapshots averaging about 20 KB. Reads made it much worse (#1803): - every history read deep-copied the whole projection before applying its limit, so the attention summary's 500-row read copied all 1,895 occurrences; - every read with a `since` (the 30-day alert-quality window, date-filtered history) built a fresh projection and re-decoded every snapshot in its window. That churn is why the same install holds about 158 MB of Go heap for 78 MB of live data. ## What changed - One full-history fold, built and extended as before. Each occurrence also keeps a small mark per folded event (time, durable ID, and the `LastSeen` it contributed) and the index of its latest acknowledged snapshot. - A `since` read is answered from that fold. The window starts at each occurrence's first event that the log's own `occurred_at >= since` text comparison admits. `LastSeen` and acknowledgement come from that point on, and occurrences are ordered by their first in-window event, exactly as a walk of only the window would produce. - Overlay, sort and limit run on lightweight rows under the projection lock. Only returned rows are copied, once, by `canonicalizeAlertHistoryForOutput`. - The alerts contract notes the windowed-read rule and the pinning tests. ## Measured (pulse-dev, log shaped like the dogfood install's: about 1,900 occurrences, 40k events) | Read | main | this PR | |---|---|---| | 30-day window | 68.9 ms, 19.8 MB allocated | 5.2 ms, 2.8 MB | | Latest 500 | 12.7 ms, 8.5 MB | 3.6 ms, 2.0 MB | | Full history | 20.8 ms, 12.9 MB | 13.4 ms, 6.9 MB | The 36 MB the fold itself holds is unchanged. The new marks add about 56 bytes per lifecycle event, about 2 MB at 40k events. ## Checks - `TestWindowedHistoryMatchesAWalkOfOnlyTheWindow` compares every window and limit against an independent walk of only the window (the original algorithm). It covers 25 random histories with acknowledgements on both sides of the boundary, out-of-order `LastSeen`, uneven fractional seconds where RFC3339Nano text order and time order disagree, same-instant events, a history clear, and live overlays. - `TestWindowedHistoryReusesTheFullHistoryFold`, `TestLimitedHistoryReadCopiesOnlyReturnedRows` and `TestHistoryReadResultsDoNotAliasTheSharedFold` pin reuse, copying and isolation. - Negative controls: ignoring the window, building a fresh fold per `since` read, and copying every row before the limit each fail their test. - Removing the per-row clone did not fail the aliasing test, because the output copy already isolates results. That double copy was dropped, and the test guards isolation end to end. - Vet and the whole `internal/alerts/...` tree pass on pulse-dev. ## Not established How much held heap this saves on the dogfood install itself. That needs this build running there. Related to #1803 |
||
|
|
43efbb9152 |
agents: name command access plainly and ask for it before the mint
The only way to give an agent the command access that Docker and Podman container updates need was a checkbox called "Enable legacy combined command profile", behind "Show advanced connection and install options", which only appeared after the install token was minted. The name told the users who want updates not to use it, and #1728 shows a user missing it entirely and hand-editing token scopes instead. It is not being retired for container updates yet. Updates dispatch through the agent command session; the separate action runner's container actions are unqualified, and rootful Docker under the safe collector profile has no update checks or actions. So the option stays, with the same security posture: off by default, an explicit choice mints an execution-scoped token and records command-capable, and a monitoring-only install still cannot be promoted remotely. The checkbox now reads "Allow Pulse to run commands on this host" and sits in the token step, so the choice is made before the credential is minted. Its description says what it enables, that Pulse can then run commands as root, and that it cannot be switched on later from Pulse. The token summary no longer claims commands can be enabled per host from Settings. One shared label feeds the installer, the locked Docker update pill and the Proxmox setup guide, and the docs name the option the same way. |
||
|
|
68a59805c9 |
build: move to Go 1.26.10 and Vite 6.4.5 for October advisories (#3092)
Go 1.26.10 (20 October) distrusts the compromised eMudhra intermediate CAs in `crypto/x509` (CVE-2026-111943). Pulse binaries built with 1.26.9 still accept chains through those intermediates on every verified outbound TLS connection. govulncheck does not report a trust-store change as a called symbol, so the scheduled scan stayed green. This moves the governed toolchain floor and every pin together: - `go.mod`, `scripts/.go-version`, the toolchain installer and the release-script guards - the source-built container stages (`Dockerfile`, provider control plane, integration mock), using the `golang:1.26.10-alpine` manifest-list digest, plus the Debian dev container - the root-pair diagnostic workflow and script, and their tests - the deployment-installability contract floor It also moves Vite from 6.4.3 to 6.4.5, which fixes the three `launch-editor` advisories behind today's failed dependency advisory watch and adds the 6.4.5 `server.fs` query check. Both affect only the local dev server. The lockfile changes only the `vite` entry, and `dependencySecurity.test.ts` now pins the floor. Checked on pulse-dev against this exact tree: the go.mod toolchain resolves to go1.26.10, `go build ./...` succeeds, the root-pair diagnostic tests pass, govulncheck is clean, the dependency floor test passes, `npm audit` reports 0 vulnerabilities, and `vite build` succeeds. `scripts/installtests` passed except `TestAutoUpdateRunningExecutableIdentity`, which hit its 15-second subprocess deadline while pulse-dev was at a load average of about 20. Rerun alone, it passed 3 out of 3 times on this tree and failed 3 out of 3 times on unchanged `origin/main` under the same load, so the timeout is load-sensitive and not caused by this change. |
||
|
|
2494930f04 |
alerts: answer history reads from the shared fold without full copies
On a long-running install the alert history projection was the largest part of the Go heap: 36 of 78 MB live on the dogfood install, one merged alert per occurrence for the event log's 90 days (#1803). Every read then deep-copied the whole projection before applying its limit, so the attention summary's 500-row read copied all 1,895 occurrences, and every Since read (the 30-day alert-quality window, date-filtered history) built a fresh projection and re-decoded every snapshot in its window. Each occurrence now keeps one small mark per folded event (time, id and the LastSeen it contributed) and the index of its latest acknowledged snapshot. A Since window starts at the first event the log's own occurred_at >= since text comparison admits, takes LastSeen and acknowledgement from there and orders occurrences by their first in-window event, so it equals a walk of only the window. Overlay, sort and limit run on lightweight rows under the projection lock, and only returned rows are copied, once, by canonicalizeAlertHistoryForOutput. On a log shaped like the dogfood install's (about 1,900 occurrences, 40k events), a 30-day read went from 68.9 ms and 19.8 MB allocated to 5.2 ms and 2.8 MB, a 500-row read from 12.7 ms and 8.5 MB to 3.6 ms and 2.0 MB, and a full read from 20.8 ms and 12.9 MB to 13.4 ms and 6.9 MB. A randomized test holds every window and limit equal to an independent walk of only the window, across acknowledgements on both sides of the boundary, uneven fractional seconds, same-instant events, a history clear and live overlays. Negative controls: ignoring the window, a fresh fold per Since read and copying every row each fail their test. Related to #1803 |
||
|
|
02e53bc777 |
build: move to Go 1.26.10 and Vite 6.4.5 for October advisories
Go 1.26.10 (20 October) distrusts the compromised eMudhra intermediate CAs in crypto/x509 (CVE-2026-111943). Binaries built with 1.26.9 still accept chains through them, so every verified outbound TLS connection Pulse makes (licence, updates, webhooks, AI providers) stays open to interception by a holder of those keys. govulncheck does not flag a trust-store change as a called symbol, so the scheduled scan stayed green while the gap existed. Advance the governed toolchain floor and every pin together: go.mod, the toolchain installer and release-script guards, the source-built container stages (manifest-list digests for golang:1.26.10-alpine and the Debian dev container image), the root-pair diagnostic, and their tests. Vite 6.4.3 bundles a launch-editor with three advisories (GHSA-587h-7wmj-h5pm, GHSA-r825-w8mm-5pxw, GHSA-698v-4r3x-3vcv) that failed the daily advisory watch. 6.4.4 bundles the fixed launch-editor and 6.4.5 adds the dev-server server.fs query check. Both reach only the local dev server, not the built product. The lockfile changes only the vite entry, and dependencySecurity.test.ts now pins the floor. |
||
|
|
6517a9d891 |
docker: offer container updates only where Pulse can apply them
The Docker table offered a live Update button for containers whose update the server had already refused, most often because the host's agent was installed without command access. The click then failed with "command agent is not connected", so on such hosts the feature never worked and never said why. The server's refusal arrives only over REST. Opening the Docker page from another platform page seeded its cache from the websocket-fed all-resources cache, which read as fresh, and websocket ticks kept it fresh, so the page never fetched REST and never learned of the refusal. Each cache entry now records when it last received a REST snapshot, and a prefer-ws-then-rest route revalidates at mount when that snapshot is missing or stale. The table also builds each row's update action reactively, so readiness that arrives after mount reaches the row. The update control now requires the server to advertise the update capability, so a host with stale inventory no longer offers it either. The locked pill reads "Update" to fit the column and explains the cause: for an agent without command access it names the host and the install option that grants it. Locally built images have no RepoDigest, so no registry holds them. The agent asked Docker Hub anyway and reported its 401 as "Check failed" on every cycle. It now reports a "local image" state without a registry request, shown as Local beside Pinned. Contract-Neutral: performance-and-scalability gains one mock fixture state with no hot-path change; storage-recovery routes keep REST-first hydration, which this change does not alter. |
||
|
|
29bf5e8a47 |
Release v6.6.0 (#3089)
Release preparation for `v6.6.0`. After this merges, the release workflow runs at this exact head `25100090d2d07f9f34daad75f098cd9bc7638c5e`. |
||
|
|
25100090d2 |
Prepare v6.6.0
Promote v6.6.0-rc.5 to stable after its 24-hour soak. Only release metadata changes. Contract-Neutral: Release-only v6.6.0 version, release notes and distribution metadata; no public install or runtime contract changes. Change-source: pulse-maintainer |
||
|
|
c3fcddf94c |
sqlite: bound every store's page cache to SQLite's default
Five stores set private page caches of 16 to 64 MB per connection. The modernc driver allocates that cache with anonymous mmap outside the Go heap, so GC and GOMEMLIMIT never see it, and each page costs about twice its size. Measured: reading a 107 MB database through a 64 MB cache_size added 117 MB of resident memory, against 6 MB at SQLite's 2 MB default. On a long-running dogfood install about 165 MB of 360 MB resident sat outside the Go heap, with a 126 MB audit database behind a 64 MB cache (#1803). Audit, RBAC, notification queue, unified resources and the alert event log (writer and both readers) now take pdb.PageCachePragma. Pages that miss it come from the kernel file cache, which the kernel can reclaim. The audit suite, including its large legacy migration, takes the same time as before (309 s on both trees). pkg/db tests reject any other cache_size in production code and bound the resident growth of a 30 MB scan. Each store's test pins its effective value. Negative controls: restoring the audit cache fails the source scan, and a 64 MB reader cache grows a scan by 55 MB against a 16 MB budget. modernc sorts _pragma values before applying them, so a second cache_size in one DSN cannot reliably override. Related to #1803 |
||
|
|
49d437331e |
commercial: give the Pro plan card a price, groups and a clear action
The Pro comparison card on Plans & Billing was a half-width box of small grey text: no price, every feature at the same weight, and a small outline link as the only way to buy. It now has a header with the plan name, a one-line tagline, the price and a primary View plans button, then the features in three labelled groups with check marks: for your team, for larger estates, and Patrol and history. |
||
|
|
197242b568 |
commercial: present Pro as the plan for teams and larger estates
The in-app Pro comparison card, now shown to free installs, led with Patrol modes and 90-day history: value an individual needs for their own setup, which reads as a withheld free version (issue #1409 called the earlier caps greedy). Paid features for an organisation read as normal, and paying installs skew to larger estates (45% against 8.5% of free installs). The card now leads with team controls, reporting and agent profiles, external probes and support, with Patrol modes, Patrol fixes and 90-day history after. Prices and entitlements are unchanged. |