Changelog
All notable changes to the pgEdge AI DBA Workbench are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[2.0.0-beta1] - 2026-11-12
Breaking changes
This release contains breaking changes, migrates the datastore and the authentication database forward only, and must be upgraded in a fixed order. Before upgrading from release 1.0.0, read Upgrading to 2.0.0.
Added
-
Publish Debian and RPM packages for the server, collector, alerter and web client, for amd64 and arm64, on Ubuntu 22.04, 24.04 and 26.04, Debian 12 and 13, and AlmaLinux 9 and 10. The service packages install systemd units, and the web client package ships an Nginx configuration. (#298, #338, #452)
-
Limit how many AI Overview summaries each user can cause the server to generate, so that a script in a loop or a browser tab that keeps reconnecting cannot run up the bill on the LLM account (#822). A user over the limit is served the cached summary for the scope, even if it is stale, or an HTTP 429 response with a
Retry-Afterheader when none is cached, in which case the AI Overview card says how long to wait; forced refreshes are throttled per user. The newllm.rate_limit.overviewsettings default to 60 generations an hour with a burst of 20, and one forced refresh every 30 seconds. -
Add a
-health-check host:portflag toai-dba-server, which checks that a listener accepts TCP connections at the address and exits. The example Docker Compose files now use it for the server's health check. -
Refuse a Debian or RPM package upgrade whose installed server configuration the new server would refuse at start-up (#784). A new
pgedge-ai-dba-upgrade-checkpackage, which every other package of the suite requires before installation, checks/etc/pgedge/ai-dba-server.yamlwith the new server before any installed files are replaced; on failure the upgrade stops with the server's error, and the installed version stays in place and keeps running. The check is skipped on a fresh installation, and protects upgrades from this release onwards; release 1.0.0 predates federated login, so an upgrade from 1.0.0 cannot meet the trusted proxy rule. Unattended upgrades throughunattended-upgradesordnf-automaticfail on every run until the configuration is corrected, and on Debian and Ubuntu the first upgrade needsapt upgradeorapt-get dist-upgrade, sinceapt-get upgradekeeps back an upgrade that adds a package. The newai-dba-server -check-configflag runs the same check by hand. See the Upgrading the Workbench page. -
Add column sorting and a query-text filter to the Top Queries section of the Server Dashboard and the Database Dashboard (#757). Clicking the Calls, Total Time, Mean Time or Rows header sorts the list by that column, largest first, and clicking the same header again reverses the order; the default remains Total Time, largest first. A Filter queries field in the section header keeps only the queries whose text contains the term, ignoring case. Sorting and filtering apply across the whole result set, and either change returns the section to its first page. The
GET /api/v1/metrics/top-queriesendpoint gained an optionalquery_textparameter, a plain substring of at most 1,000 characters matched against the statement text with passwords masked; a longer term, or one that is not valid UTF-8 or contains a NUL character, returns status 400. -
Compare a metric with its anomaly baselines on the server dashboard (#348). The new Baseline comparison section plots a baselined metric against the band of normal values (the baseline mean plus or minus 2, 3 or 4 standard deviations) that the alerter's tier 1 detection uses, with a choice of baseline model (as scored, day of week, hour of day or overall), faded stretches where a baseline is not yet warm, solid markers for points that raised an anomaly alert and hollow markers for points outside the band with no alert. An alert raised before the window covers the points up to where it cleared, and hourly metrics include the hour that contains the window start. The server provides the data through the new
GET /api/v1/metrics/anomaly-comparisonandGET /api/v1/metrics/anomaly-comparison/metricsendpoints, and scores with the alerter's own code, which moved to the sharedpkg/anomalypackage together with the baselined metrics' historical queries. The alerter now publishes its effective scoring settings to the newalerter_settings.tier1_scoringcolumn (collector schema migration 31) when it starts and when it reloads its configuration; until it does, the view uses the default settings and says so. Upgrade the collector before, or together with, the alerter; an alerter upgraded first logs a warning that it could not publish its settings. TheGET /api/v1/metrics/baselinesendpoint also accepts the newperiod_typeanddatabase_nameparameters, andGET /api/v1/alertsaccepts the newactive_startandactive_endparameters, which return the alerts active at any instant in a range. -
Configure reasoning effort, prompt caching and extra request body fields for LLM requests (#726). The server's
llmsection, and each of the alerter'sllm.openai,llm.anthropic,llm.geminiandllm.ollamasections, acceptreasoning_effort(none,minimal,low,medium,high,xhighormax),prompt_caching(autooroff) andextra_body, a mapping of fields added to each request theopenaiprovider sends, for options such as an OpenAI-compatible gateway's. Withprompt_caching: auto, Anthropic caches the tool list, the system prompt and the conversation, so each step of an Ask Ellie tool loop reads them from the cache; in the alerter it caches the classification system prompt. All three are unset by default, leaving requests as they were. An unknown value,streaminextra_body, or anextra_bodyvalue that cannot be sent as JSON stops the server or alerter from starting, and the alerter rejects a reload that contains one; changing a setting takes a restart. -
Store metrics in pgEdge ColdFront (#755). The new collector option
use_coldfront_for_metrics(defaultfalse), or the-use-coldfront-for-metricsflag, creates eachmetrics.*table as a PostgreSQL view over an Apache Iceberg table on object storage; the server and alerter need no setting. The datastore needs ColdFront 1.0.0-beta3 or later, prepared before the collector's first start; the operator must schedule ColdFront's compactor, and an existing datastore cannot switch between storage modes. The Storing Metrics in pgEdge ColdFront page describes the prerequisites, the setup and every difference from a standard datastore. -
Classify tier 3 anomaly candidates with an optional decision model (#626). The new
llm.decision_provideroption selectstypesafe(the TypeSafe Jev models, configured in the newllm.typesafesection withapi_key_file,base_urlanddecision_model, defaultjev-latest) oropenai(the OpenAI Decisions API, with the newllm.openai.decision_modeloption, defaultgpt-6-luna); leaving it empty,noneordisabledkeeps the existing behaviour. The decision model returns the probability that a candidate is a real issue, and tier 3 suppresses the candidate when one minus that probability reaches the newanomaly.tier3.decision_suppress_thresholdoption (default 0.5). Any decision model failure falls back to the reasoning model for that candidate, or raises the alert when no reasoning provider is configured; re-evaluation of acknowledged alerts still uses the reasoning model. A failing decision provider raises its ownTier 3 decisionprovider health system alert. Collector schema migrations 22 and 23 add thetier3_sourceandtier3_modelcolumns toanomaly_candidates, recording which kind of model, and which model, decided each candidate; upgrade the collector before, or together with, the alerter. An alerter upgraded first logsERROR: Failed to get anomaly candidatesonce each cycle and raises no anomaly alerts, and its queued candidates expire unprocessed after two and a half hours. Changing the decision provider takes an alerter restart. In a spike on 150 real candidates, the Jev model agreed with the stored reasoning-model decisions 89% of the time, at about a fiftieth of the cost, at October 2026 prices, and a fifteenth of the median latency of Claude Haiku 4.5, sotypesafeis the recommended provider. -
Explain why an anomaly alert was raised (#626). Anomaly alerts in the status panel have a "Why was this flagged?" button that shows the reasoning behind the tier 3 decision, served by the new
GET /api/v1/alerts/reasoningendpoint. A reasoning model verdict returns the model's stored reasoning. A decision model verdict carries no explanation, so the first request asks the LLM configured in the server'sllmsection to write one from the alert's stored data, and stores it in the alert'sanomaly_detailsundertier3_reasoningfor later requests. Without a server LLM, a decision model verdict has no explanation. Any user who can see an alert can view, and so trigger generation of, its reasoning. Logging out now also clears the client's cached query analyses and SQL validation results, as well as the cached reasoning, so the next user to log in on the same browser cannot see them. -
Add the
describe_datastoreMCP tool, which lists the sources thatquery_datastorecan read, with each source's kind, time column and scoping rule, and describes one source's columns (#566). The newbuiltins.tools.describe_datastoreoption switches the tool off, the tool is a grantable MCP privilege that anyone holdingquery_datastorealso has, and Ellie can call the tool. -
Add a Run button to SQL code blocks in Ellie's chat replies (#623). The query runs on the connection named by a leading
-- connection_id: Ncomment when the reply's tool calls used that server, or on the single server and database that the reply's tool calls used; when the target is unclear the block shows a server selector instead. Results appear inline and are not sent back to Ellie. SQL with placeholders is labelled a template with no Run button; besides$1, this now covers<name>,{{name}},${name}and:nameplaceholders in every view. The write confirmation prompt now names the server and database the statements will run on. -
Raise a system alert when anomaly detection is degraded by a failing embedding or reasoning provider (#582). The alerter counts consecutive failures for each tier (tier 2 embeddings, tier 3 classification and re-evaluation) and provider, treating a tier 3 or re-evaluation response that holds no decision the alerter can parse as a failure (#594), and raises one warning alert for the tier and provider when the count reaches the new
anomaly.provider_health.failure_thresholdoption (default 3), or at once when the embedding or reasoning provider fails the new startup health check. The alert is also raised for a provider that fails a steady share of its calls without failing consecutively (#593): the newanomaly.provider_health.failure_rate(default 0.3) andanomaly.provider_health.failure_rate_window(default 20) options raise it once that share of the last window of calls has failed, and keep it open until fewer have. The alert names the provider, the model, the failure count and the last error, clears on the next successful call to the provider (from either tier 3 or re-evaluation for the reasoning provider), is not raised again within five minutes of clearing, and goes to the estate default notification channels. System alerts use the newsystemalert type and have no connection, which collector schema migration 19 allows; every user and API token with alert access sees them, the REST API reports anullconnection_idfor them, andGET /api/v1/alerts/countsgains asystemtotal. Acknowledging or restoring a system alert requires a superuser or themanage_alert_rulesadmin permission, bounded by an API token's admin scope, and the server refuses to save AI analysis on a system alert with a 400 response. The web client lists system alerts on the estate view with an "AI DBA Workbench" source label and a "System" type chip in place of a server name, offers no AI analysis for them, since they concern no monitored connection, and shows their acknowledge and restore actions only to a superuser or a holder ofmanage_alert_rules. -
Add the
probe_unavailablebuilt-in alert rule, which reports a probe that had been collecting and has stopped being available, for example because an extension, a privilege or the connection has gone (#512). Collector schema migration 16 seeds the rule in theavailabilitycategory on the newprobe_availablemetric, which reads 0 whilst the probe is unavailable and 1 whilst it collects, at warning severity and enabled by default. The rule fires only for a probe that has collected at least once, so a probe whose extension was never installed never raises one. The alert clears when the probe collects again, or when an operator disables the probe, stops monitoring the connection or removes the availability row. A heldmetric_stalenessalert and aprobe_unavailablealert may be active on the same probe at once, and per-connection threshold overrides, blackouts and the alert cooldown apply to the new rule as they do tometric_staleness. -
Add federated login through an OpenID Connect identity provider, configured in the new
http.auth.oidcsection and offered as a second button on the login page. The Workbench discovers the provider at start-up, runs an authorisation code flow with PKCE, matches the account on the issuer andsubclaim, and can provision an account on first login. Provider groups map onto Workbench groups throughgroup_map, andsuperuser_groupnames one provider group that confers superuser; both are reconciled at each login, and a group the map does not name is left alone. Access may be restricted withallowed_email_domains, which matches exact domains and requires the provider to have verified the address. Authorisation is otherwise unchanged, and service-account API tokens remain the mechanism for MCP clients. Local username and password login is unaffected and stays on unlesshttp.auth.local.enabledis set tofalse. The new Single Sign-On page in the Administrator's Guide covers the configuration, provider registration and the operational limits, including the need forhttp.trusted_proxies, the username rule the chosen claim has to satisfy, and why a local break-glass administrator should be kept. The server warns at start-up when federated login is enabled without a trusted proxy list, whensuperuser_groupis set, and it names the host thatredirect_urlsends authorisation codes to, so that a typo there is caught before the first login fails. A federated session ends when the identity provider stops vouching for the user: the login requests theoffline_accessscope, the session expires with its ID token, and the server then redeems the provider's refresh token, ending the session if the provider refuses, or in any case 24 hours after the login; a refresh also reconciles the mapped groups and superuser flag when the new ID token carries the groups claim. Theauth_paramsoption appends extra parameters to the authorisation request, such as theaccess_type: offlineGoogle needs before it issues a refresh token, andallow_api_tokens,falseby default, decides whether a federated account may own API tokens (#742). Theprovision_users,allowed_email_domains,superuser_group,group_mapandbutton_labeloptions, and switching federated login off, take effect on aSIGHUPreload, whilst the provider connection settings need a restart (#484). The server refuses to start with federated login enabled, anhttpsredirect_url,http.tls.enabledset tofalseand nohttp.trusted_proxies, unlesshttp.auth.oidc.allow_unprefixed_state_cookieis set totrue(#506). The API, the-list-userscommand and the Users page of the Administration console show whether each account signs in locally or through the provider, through the newauth_sourceandauth_issuerfields, anAuthenticationcolumn and aFederatedchip (#483). The authentication database schema moves to version 4 on the first start after upgrading, adding anauth_sourceand anexternal_subjectcolumn tousers; every existing installation runs that migration, and the database cannot afterwards be opened by an older server. (#261) -
Add
-link-oidc-userand-unlink-oidc-usercommands to the server, which attach an existing account to an identity provider subject and detach it again. Linking is how an account is reached by a federated login wherehttp.auth.oidc.provision_usersis left at its default offalse: the operator reads the issuer and subject from the refused login's server log line, which now quotes both and spells out the command, and links the pre-created account. Moving an account that is already linked requires the-relinkflag, and a service account cannot be linked at all. Wherehttp.auth.oidc.allow_api_tokensis on, linking leaves the account's existing API tokens working at full privilege, so an operator tightening an account's authentication has to remove them with-remove-tokenthemselves. Unlinking returns the account to local authentication, replaces its password hash with an unusable one and revokes its API tokens, so the account is reachable by nothing until a password is set with-update-user;-restore-passwordconverts the account back to local login instead, keeping both the password it held before it was linked and its tokens. Neither command ends a browser session the running server has already issued, because sessions live in that server's memory and the command runs in its own process, so offboarding means unlinking and then disabling the account with-disable-user, which the server honours from the next request onward. Note that linking an existing account hands its mapped group membership, and its superuser flag whereversuperuser_groupis configured, to the identity provider from the next federated login onward, which is why the local break-glass administrator should not be linked. (#261) -
Add a
-group-descriptionCLI flag that sets a group's description when creating it with-add-group, matching the description support already available in the web console. (#301) -
Add a
-list-membersCLI flag to the server, which lists the members of the group named with-group, including both member users and nested member groups. (#303) -
Add pagination and database filtering to the Top Queries panel on the server dashboard. The panel footer now provides a page size selector offering 10, 20, 50, or 100 rows, defaulting to 20, along with previous and next controls and a "Showing X-Y of Z" indicator, so users can page beyond the first rows. The panel header shows a database filter when the connection monitors more than one database; the filter defaults to "All databases". The
GET /api/v1/metrics/top-queriesendpoint gained theoffsetanddatabase_namequery parameters, and now returns anX-Total-Countresponse header that reports the total number of matching rows; the JSON response body is unchanged. (#335) -
Add a custom time range to the monitoring dashboards and the event timeline, alongside the existing
1h,6h,24h,7d, and30dpresets. The new Custom option opens a picker with From and To fields, so that the graphs can be lined up with a known incident window rather than a rolling window ending at the present moment. The/api/v1/metrics/queryendpoint now acceptstime_range=customtogether with RFC 3339time_startandtime_endparameters. The server rejects a span longer than 366 days and a start at or after the present moment, and it clamps an end in the future to the present moment, since a picker set to the current day routinely overshoots by a few minutes. The/api/v1/timeline/eventsendpoint applies the same rules to itsstart_timeandend_timeparameters, and the picker applies them too, keeping Apply disabled and naming the rule that failed. Auto-refresh is suspended whilst a custom window is active, because re-fetching a fixed historical window returns identical data on every poll; the selector shows a pause indicator, and refreshes resume as soon as the user returns to a preset. The window is held in memory only, so it is neither persisted nor reflected in the page URL, and a reload returns to the last preset. The time-series charts honour the dashboard window, including the charts on the query detail overlay, whilst the event timeline honours the independent window set on its own control. (#345) -
Add a Connections section to the server dashboard, which groups the server's client connections by database user, client address, or database, and breaks each group down into total, active, idle, idle in transaction, and other backend states. The counts come from the most recent
pg_stat_activitysnapshot within the selected time range, and the By Client tab also shows the reverse-resolved client hostname where the server recorded one. A newGET /api/v1/metrics/connection-groupsendpoint serves the section, returning at most 200 groups alongside atotal_groupscount so that a truncated response can be recognised, and it honours a custom time range through the sametime_startandtime_endparameters as the metrics query endpoint. (#346) -
Add an
llm.max_tokenssetting to the alerter, which caps the output tokens of a reasoning call. The cap applies to tier 3 anomaly classification and to acknowledged-alert re-evaluation. The setting defaults to4096tokens and replaces a hardcoded 500-token limit. A reasoning model could consume that smaller limit entirely on its thinking block, leaving no room for the classification verdict. A value of zero or less selects the default. (#399) -
Add a
_deltaderived metric to the metrics query API (GET /api/v1/metrics/query). A metric named<column>_deltareports the increase of a cumulative counter within each time bucket, which suits a bar chart of rare events such as checkpoints; a bucket containing no sample reports zero, a connection with no samples in the window returns no data points, and theaggregationparameter does not apply. The_per_secand_deltaforms compute the change of each monitored entity's counter separately before adding them together, so a network interface or database that appears or disappears between samples no longer produces a spike or a stale value, and the sample before the window is only used when it is recent enough not to inflate the first bucket. (#400) -
Add per-query detail to the query drill-down on the object dashboard. The drill-down now shows minimum and maximum execution time as the "Min Time (All Time)" and "Max Time (All Time)" KPI tiles, which report lifetime figures because
pg_stat_statementscannot recover an extreme for a narrower window. The drill-down also shows the database role that ran the query beside the query text, and displays "Unknown" when the role cannot be resolved. The API gained aGET /api/v1/metrics/query-statsendpoint that returns the period-scoped average execution time for a single query, for a preset or a custom time window and optionally restricted to one database, and theusername,min_exec_time, andmax_exec_timefields onGET /api/v1/metrics/top-queriesrows. Thequeryidparameter of that endpoint and of the new one must now be a 64-bit integer; any other value is rejected with a400instead of matching nothing. (#350) -
Add
_pctand_sessionsderived metrics to the metrics query API (GET /api/v1/metrics/query). A metric named<column>_pctreports the share of wall-clock time that a cumulative millisecond column such asblk_read_timeadvanced by, and<column>_sessionsreports the average number of sessions in a state from thesession_time,active_timeandidle_in_transaction_timecolumns ofpg_stat_database. Each series in the response now carries aunitfield (/s,ms,%,sessionsor empty). The collector recordspg_stat_statements_info.stats_resetin a newstats_resetcolumn of thepg_stat_statementsprobe where the extension is version 1.9 or later, as shipped with PostgreSQL 14; the column is added by collector schema migration 10. (#402) -
Add client attribution to the query drill-down on the object dashboard, which now shows a Last Observed Client value beside the Database User value. The value names the client last seen running the statement, as "hostname (address)" where the monitored server resolved a hostname, and as "local" where that client connected over a Unix-domain socket on the database host. The client is attributed per database and role as well as per query identifier, so a query in one database is never credited to a client that only ever connected to another. The collector's
pg_stat_activityprobe collects thequery_idcolumn to support this, added tometrics.pg_stat_activityby schema migration 12, and the rows returned byGET /api/v1/metrics/top-queriesgained the nullableclient_addr,client_hostname, andclient_observed_atfields. The attribution is best effort, becausepg_stat_activityis sampled periodically: a client is captured only for statements that were in flight when a sample was taken, and the value names the client seen most recently rather than the only one that ran the query;client_observed_atis null exactly when no sample ever caught the query in flight, which is the field to test for that case. Nothing is attributed on PostgreSQL releases before 14 or on servers that run withcompute_query_idoff, where the drill-down reads "Not observed", so operators who want the attribution should enablecompute_query_idon each monitored server. (#384) -
Add an estimated available memory figure to the memory metrics. The collector schema migration 13 adds an
available_memorycolumn tometrics.pg_sys_memory_info, and thepg_sys_memory_infoprobe populates the column with free memory plus cached memory, storing NULL whenever either input is missing. The Memory Usage chart on the server dashboard plots the figure as a fourth series, "Available (est.)", alongside Used, Free and Cached, and the memory KPI tile keeps the usage percentage as its headline and shows the available figure as a secondary line. The value is an estimate rather than the kernel'sMemAvailable, which thesystem_statsextension has never exposed; free memory alone reportsMemFree, which counts only memory that is entirely unused, whilst the estimate also counts reclaimable page cache, and the estimate runs high on a host with a large non-reclaimable slab or a largely dirty page cache. (#429) -
Add Telegram as a notification channel type, alongside the existing email, Slack, Mattermost and webhook channels. A Telegram channel delivers through the Bot API rather than an incoming webhook, so it stores a bot token, encrypted with the server secret and never returned by the API, together with a chat ID that is either a numeric ID or an
@channelusername. The create and update endpoints accept thetelegram_bot_tokenandtelegram_chat_idfields, a response reportstelegram_bot_token_setin place of the token, andPOST /api/v1/notification-channels/{id}/testsends a test message to the configured chat. Notifications are sent with thesendMessagemethod in HTML parse mode, so a Telegram template renders the message text rather than a JSON body. A message beyond Telegram's 4096-character limit is truncated without cutting an HTML entity or an opening tag in half, and a message Telegram refuses to parse is sent once more with no parse mode, so a template with broken markup delivers the alert as plain text rather than losing it. The admin panel gains a Telegram Channels tab, and collector schema migration 14 adds thetelegram_bot_token_encryptedandtelegram_chat_idcolumns tonotification_channels. (#475) -
Add an audit log for RBAC changes (#65). Every create, update, delete, enable, disable, grant, revoke and scope change on users, tokens, groups and permissions is recorded with the acting user, token or CLI user, the client address, and the before and after state, together with refused requests. The group, superuser and account changes a federated login makes, and the
-link-oidc-userand-unlink-oidc-usercommands, are recorded too (#743, #748). Superusers can read the log from the new Audit Log tab in the admin panel, fromGET /api/v1/rbac/audit, or with the-list-auditserver flag. Repeated denials and failures are coalesced into one event per minute carrying adetails.repeat_count, so a client retrying a refused request cannot flood the log. The log is a tamper-evident hash chain keyed by the server secret, which-verify-audit-logchecks: it exits with status 3 when the secret appears to be wrong or rotated, 2 when events have been altered or deleted, and 1 for any other failure.-rechain-audit-logre-anchors a log that no longer verifies, such as one written partly under a previous server secret. Retention is controlled by the newhttp.auth.audit_retention_dayssetting, which defaults to 90 days. Every server command-line subcommand now needs the server secret file, as described under Changed. -
Add a Top Queries section to the Database Dashboard, listing the slowest statements recorded for the database you are viewing, ordered by total execution time. The section follows the dashboard's time range, pages through the results twenty rows at a time by default, and hides the Workbench's own monitoring queries unless you switch that off. Selecting a row opens the existing query detail overlay. The Server Dashboard's Top Queries section is unchanged, and keeps its database filter. (#369)
-
Add a Replication Slots section to the server dashboard, after WAL and Replication (#347). It lists the server's replication slots from the collector's latest
pg_replication_slotssnapshot with each slot's type, whether a consumer is connected (Active or Inactive), its WAL status (Reserved, Extended, Unreserved or Lost), the WAL it retains and its safe WAL size, with a summary of the active, inactive and at-risk slots. Above the table it names the servers this one replicates from and the standbys and subscribers that replicate from it, as the cluster topology records them. The collector does not record slots that have never reserved WAL, and usually stops recording a slot once it loses its WAL; a dropped or lost slot can still be listed with its last recorded state for up to an hour.
Changed
-
Breaking: add the
connection_security.ssl_cert_diroption to the server and the collector, which names the absolute directory that the SSL certificate, key and root certificate files of monitored connections must live in. On the server, thePGEDGE_CONNECTION_SECURITY_SSL_CERT_DIRenvironment variable and the-connection-ssl-cert-dirflag also set it; on the collector, the-connection-ssl-cert-dirflag sets it, and there is no environment variable. The option is empty by default, and while it is empty each component refuses every SSL file path except a root certificate value ofsystem, so an existing connection that names an SSL file stops working until the option is set on both components and the files are kept inside the directory at the same paths on both hosts. A relative path stops the component from starting, and each component reads the option at start-up only. See Upgrading to 2.0.0. -
Breaking: treat the IPv6 unspecified address
::, the IPv4-compatible range::/96, the NAT64 prefixes64:ff9b::/96and64:ff9b:1::/48, the 6to4 range2002::/16, the SIIT range::ffff:0:0:0/96and the site-local rangefec0::/10as internal addresses. A deployment that reaches monitored servers through NAT64 must setconnection_security.allow_internal_networkstotrueor add the NAT64 prefix toallowed_hosts. The alerter refuses to deliver a notification to these ranges whatever those settings say, so an IPv6-only alerter host that reaches IPv4-only notification providers through NAT64 stops delivering until the alerter'snotifications.allow_internal_networksoption istrue. That option still leaves::refused, along with any address in::/96,::ffff:0:0:0/96,64:ff9b::/96or2002::/16that embeds an unspecified, link-local or metadata IPv4 address, and any address in64:ff9b:1::/48whose IPv4 reading at any RFC 6052 offset is link-local or100.100.100.200; the option permits every other address in64:ff9b:1::/48, like the other internal ranges. See Upgrading to 2.0.0. -
Breaking: check the address of every alerter notification delivery, for webhook, Slack, Mattermost, Telegram and email (SMTP) channels, and refuse private, loopback and other internal addresses by default. Release 1.0.0 checked only webhook channels. An email channel that uses an internal SMTP relay, or a Mattermost channel that points at a self-hosted server on the local network, stops delivering until the new
notifications.allow_internal_networksalerter option is set totrue. Release 1.0.0 already refused a webhook channel whose endpoint is an internal address, with no way to permit one; the new option now permits such an endpoint. The option defaults tofalse, has no flag or environment variable, and is read at start-up only; unspecified, link-local and cloud metadata addresses stay refused even when the option istrue. A refused delivery records the generic error "host resolves only to addresses that connections are not permitted to". See Upgrading to 2.0.0. -
List and serve only the collector's probes from the metrics schema (#755). The
list_probes,describe_probe,query_metrics,describe_datastoreandquery_datastoretools and theGET /api/v1/metrics/latestandGET /api/v1/metrics/queryendpoints now admit only the probe names the collector writes, on plain PostgreSQL and ColdFront datastores alike; other tables, partitions and views in themetricsschema are no longer listed or served. -
Build the server, collector and alerter container images on
alpine:3instead ofdebian:bookworm-slim, removing the Debian packages that carried most of the images' reported vulnerabilities, including many with no fix available. The images keep their user ID and group ID (999), so existing volumes remain writable. The client image now upgrades every Alpine package when it is built. -
Build anomaly detection baselines for four more metrics (#490):
pg_stat_replication.lag_bytes,pg_stat_checkpointer.checkpoints_req_delta,pg_stat_statements.slow_query_count(per server, #640) andpg_stat_activity.max_lock_wait_seconds, which were previously never scored by tier 1. Each historical query computes the same statistic as the metric's latest query: the largest lag across standbys, requested checkpoints counted within each hour and leaving out hours spent in recovery, slow statements counted from per-interval mean execution times, and the longest lock wait in samples where a backend waits on a lock. The livelag_bytesvalue now reads only each server's newest sample, so a standby that disconnects whilst another stays attached drops out of it at the next sample rather than holding it up for five minutes; when the last standby leaves, the probe stores nothing and the previous sample still stands for up to five minutes. -
Breaking: the
query_datastoreMCP tool takes structured arguments instead of SQL (#566). A client that sent asqlorqueryargument must now name asourceand describe the query with structured parameters, as described under Security (GHSA-2h8m-pf3h-xwpf). -
Add hysteresis to threshold alerts (#614). The alerter now raises a threshold alert only after the threshold has been breached on
threshold.trigger_countconsecutive metric samples (default 2), and clears it only after the metric has been back within the threshold onthreshold.clear_countconsecutive samples (default 3), so a metric hovering near its threshold no longer raises and clears an alert every few minutes. A sample is one collection by the collector, so the evaluator and cleaner reading the same sample again count it once. Setting both options to 1 restores the previous behaviour. The counts are held in memory and start again when the alerter restarts, and they do not apply to themetric_stalenessandprobe_unavailablerules or to connection error alerts. Theconnection_utilization_percentmetric now reports the time of its sample rather than the time of the query, so that its samples can be counted. -
Retune four built-in alert rules that fired on conditions nobody could act on (#616).
cache_hit_ratio_lownow only judges an interval that read at least 100 blocks per second from outside shared buffers as well as accessing 10,000 blocks, and its defaults drop from a warning below 80 percent to information below 50.dead_tuple_ratioonly judges tables with at least 10,000 dead tuples, and its default threshold rises from 20 percent, which is autovacuum's own default trigger, to 50.autovacuum_not_runningonly counts a table that has been past its autovacuum trigger in every sample for at least 30 minutes.checkpoint_warningskips servers whose most recent recovery state shows them in recovery, on every supported PostgreSQL version. Collector schema migration 21 changes each default only while it still holds the old shipped value, so a threshold or severity changed through the API is kept, and leaves per-server, per-cluster and per-group overrides alone. An alert on any of the three gated metrics now clears when the metric stops reporting a row for it, for example when the read rate falls, the table is vacuumed or a primary becomes a standby. -
Raise the default size of the server's datastore connection pool,
database.pool_max_conns, from 4 to 20 (#478). Every API request that reads the datastore shares that pool, and a single dashboard page load issues more than twenty metrics requests at once, so four connections made a page wait on itself and let a few slow requests hold up the whole API. Twenty, alongside the collector's 25 and the alerter's 10, stays well inside the PostgreSQL defaultmax_connectionsof 100. A configuration that leaves the option unset now gets 20 rather than the connection library's own default; one copied from the earlier example still setspool_max_conns: 4explicitly and keeps four until the line is changed or removed. The datastore pool is sized at start-up, so a configuration reload withSIGHUPdoes not resize it and the server has to be restarted. An explicitpool_max_connsalso sizes each per-session pool that the server opens to a monitored database, including pools opened after aSIGHUPreload, whilst an unset one leaves those pools at the connection library's default; a database configured only through the-db-*command-line flags now leaves it unset too, where it previously set four. -
Decide connection access and connection visibility with one rule, so that every listed connection can be opened and every connection that an account can open is listed (#592). An
All Connectionsgrant no longer restricts every connection: before this change, a single such grant to any group refused every user outside that group every connection, shared connections and the user's own included, whilst the connection lists still offered them. Only a grant that names a connection now restricts it. AnAll Connectionsgrant atreadtherefore no longer caps its members: a shared connection that no grant names stays open atread_writeto every account, subject only to an API token's own scope, so to keep a group read-only on a connection, grant that connection to the group by name. The owner of a connection always holdsread_writeaccess to it, whether or not a group restricts it. Holding a grant on one connection no longer hides shared connections that no group restricts, which the account could already open. Revoking anAll Connectionsgrant never lifts a restriction, so an API token needs onlyreadto revoke one, whilst revoking a connection's last specific grant needsread_writeeven when anAll Connectionsgrant remains. See the permission model. -
Follow the dashboard time range selector in the database summaries section of the server dashboard, the Transaction Rate tile of the estate dashboard and the comparative charts of the cluster dashboard. All three asked for a fixed twenty-four hours and so ignored the selector sitting on the header of the Monitoring section that holds them. The estate dashboard's other KPI tiles report the estate as it stands now, whatever the selector reads, because none of those figures is drawn from a window. The
/api/v1/metrics/database-summariesendpoint now resolves its window through the same rules as every other windowed endpoint, acceptingtime_range=customwithtime_startandtime_endalongside the existing presets and still defaulting to24h. Every figure the endpoint returns is bounded by that window, so a window ending in the past reports the database sizes, connection counts, dead tuple ratios and transaction rates of that moment rather than of the present one; a window ending now is unchanged. Theactive_connectionsfield of/api/v1/metrics/performance-summary, which feeds the Connection Count chart of the cluster dashboard, follows the same window as the rest of that response, so a window holding no collected sample reports zero rather than the live backend count, and the chart stays empty alongside the three series beside it. The performance tiles in the status panel deliberately keep a fixed twenty-four hour window, because those tiles render above the Monitoring section and a user reading one cannot see the control that would be changing the figure, and the database filter on the Top Queries panel keeps one too, so that the available choices do not vanish as the window narrows. The dashboard pages in the User's Guide record which panels follow the selector and which do not. (#387) -
Breaking: every server command-line subcommand, including
-add-user,-add-token,-set-superuserand-list-audit, now requires the server secret file, because the audit log is keyed by the server secret (#544). An installation that has never created one will see those commands stop working at this upgrade; create the secret file at/etc/pgedge/ai-dba-server.secret, or whereversecret_filenames, with the same contents the server and collector use. The commands report the paths they searched when they cannot find one. -
Change the alerter's default reasoning models: Anthropic's from
claude-3-5-haiku-20241022toclaude-haiku-4-5, and Gemini's fromgemini-2.5-flashtogemini-3.8-flash. Anthropic has retiredclaude-3-5-haiku-20241022, and Google no longer offersgemini-2.5-flashto new API keys, so a deployment that leftllm.anthropic.reasoning_modelorllm.gemini.reasoning_modelunset failed every reasoning call with a 404. A deployment that leaves either option unset now uses the new model; set it explicitly to keep another, and update any configuration that names a retired Claude 3 model. The example configuration and the walkthrough script are updated to match. (#459) -
Change the server's default Anthropic model from
claude-sonnet-4-5toclaude-sonnet-5, and the alerter's default OpenAI reasoning model fromgpt-4o-minitogpt-6-luna. A deployment that leavesllm.modelorllm.openai.reasoning_modelunset now uses the new model; set either option explicitly to keep the previous one. The example configurations and the walkthrough script are updated to match. -
Support models that reject the
temperatureormax_tokensparameter, such as Claude Sonnet 5 and OpenAI's GPT-6 models, which previously failed every request from the server and the alerter. The request is adjusted after the first rejection, which costs one extra round trip per client. Those models accept only their own default temperature, sollm.temperaturehas no effect on them. -
Report the queried time window in the
/api/v1/metrics/queryresponse. The endpoint returned a bare array of series and said nothing about the window behind the series, so charts derived their x-axis from the points that came back and every time range rendered identically on an instance holding little history. The bucketed response is now an object carryingprobe_name,connection_ids,time_range,time_start,time_end,bucket_seconds,bucketsandaggregationalongside theseriesarray. A bucket that collected no data, and that has no earlier value to carry forward, now reports a nullvalueinstead of dropping the point, and a point carried forward from an earlier sample is markedfilled. The latest-rows mode of the same endpoint, which a request selects by passinglimitororder_by, is unchanged. The dashboard charts now anchor their x-axis to the returned window, draw null buckets as gaps, and mark carried-forward stretches distinctly. (#430) -
Enforce an API token's connection scope on connections that belong to no group. Both the access check and the visible connection list previously admitted an ungrouped connection on ownership or the shared flag alone, without consulting the caller's token scope, so for those connections a token's connection scope was dead configuration on every surface. Both paths now intersect the scope, denying an out-of-scope connection and applying the scope's access ceiling otherwise. This is a real change for anyone handing out narrowly scoped tokens: a token that has been reaching an ungrouped connection outside its scope, or reaching one at
read_writewhilst scoped toread, will now be refused or capped. Review the scope of every token that touches a connection with no group before upgrading. A token owned by a superuser was left out at the time, because the superuser check returned before the scope was consulted; that bypass is closed separately, so such a token is now held to its connection scope as well. (#261) -
Serve
GET /api/v1/capabilitieswithout authentication. The endpoint previously required a session or API token, although the login screen has to read it before either exists in order to learn which sign-in methods to offer, so it now joins the public paths alongside login, logout and the health check. It reports the AI feature flag, the iteration limit and the newauthblock, which carries onlylocal_enabled,oidc_enabledand the button label; the provider issuer, client secret and claim mapping are never included. (#261) -
Log sessions out when the authentication database cannot be read. A session whose user record failed to load previously produced a context carrying the username with no user ID and no superuser flag, which quietly narrowed what the session could do whilst still authenticating it. Such a session is now rejected outright, so a database blip ends sessions rather than silently changing their privileges. The failure is logged with the database error, and is told apart from a deleted account and from a plainly bad token, so a run of refused requests can be traced to its cause. (#261)
-
Count deadlocks and temporary files per hour in the alerter. The
deadlocks_detectedandtemp_files_createdrules compared the change inpg_stat_databasebetween two consecutive samples against their thresholds, so the value they evaluated depended on the probe interval, and a counter reset could produce a negative delta. Both rules now sum the positive changes recorded in the last hour for each database, in line with the checkpoint and archive rules, and report their units asdeadlocks/hourandfiles/hour. The thresholds themselves are untouched, and so are any operator overrides, buttemp_files_creatednow compares its threshold of 100 against a much larger number: at the shippedpg_stat_databasesampling interval of 300 seconds, twelve samples fall inside the window, so a database that steadily creates 300 temporary files an hour reported about 25 before this release and reports 300 now. That rule will fire on databases where it never fired before. To keep the previous sensitivity, multiply the threshold by 3600 divided by the sampling interval in seconds, which gives a threshold of 1200 at the default interval. Thedeadlocks_detectedrule is unaffected, because its threshold of 0 fired on any positive change under the old comparison as well. (#409) -
Extend the
-show-group-privilegesCLI command to also display a group's admin permissions, alongside the MCP and connection privileges it already reports. (#303) -
Drop the runtime dependency on a knowledgebase package from the
pgedge-ai-dba-serverpackage in both the Debian and RPM packaging. The knowledgebase is now published as co-installablepgedge-ai-kb-<provider>-<model>packages, one per embedding provider and model, and it remains opt-in and disabled by default. Operators who want the knowledgebase install the package matching their chosen embedding provider and model, and setdatabase_paththemselves. (#316) -
Relabel the token scope options in the API token create and edit screens so they now read "All the owner's MCP privileges" and "All the owner's admin permissions" rather than "All MCP Privileges" and "All Admin Permissions"; the new wording clarifies that a token grants the owner's privileges rather than all system privileges. (#323)
-
Ellie now gives correct, deployment-aware guidance for restarting the Workbench's own components (server, collector, alerter, and web client), and no longer suggests commands for unrelated third-party tooling. (#329)
-
Plot rates rather than raw cumulative counters on the dashboard charts that are built on
pg_stat_*counters, so that a chart reflects the current workload instead of an ever-rising total. The server dashboard now plots commits and rollbacks, blocks hit and read, tuple operations, WAL bytes and records, checkpoint buffers written, and network throughput per second; the database dashboard plots commits and rollbacks per second and reports transactions per second; and the object dashboard plots query calls per second. The Checkpoints chart is now a stacked bar chart of the timed and requested checkpoints in each interval, a Checkpoint Buffers Written chart has been added, and the Checkpoints KPI has become Requested Checkpoints, the percentage of checkpoints in the selected range that were requested rather than timed; a high percentage suggests thatmax_wal_sizeis too low. A chart whose metric a probe does not collect now shows the error the server returned instead of a generic "No data" message, and a bar or area chart whose values are all zero no longer draws a y-axis running below zero. (#400) -
Make the
valueof a data point in the metrics query response nullable, and emit every time bucket in every series so that all series in a response share the same bucket times. A bucket isnullwhen two consecutive samples are more than three collection intervals apart, when a probe'sstats_resetmarker changed between them, or, for a raw column, when the last observed value is more than three collection intervals old; a rate or delta series no longer repeats a stale value into an empty bucket. The requested bucket count is clamped so that no bucket is narrower than the probe's collection interval. Thepg_sys_network_infoprobe leaves theloandlo0loopback interfaces out of every query. A_per_secrequest on a column that is not a cumulative counter, such as a gauge ormean_exec_time, is now rejected with HTTP 400 and a message naming the column's kind, rather than returning a meaningless series. The dashboards draw anullbucket as a break in a line or a missing bar. The interval a sample is judged against is the configured one widened to the spacing the samples in the window actually show, so tightening a probe's collection interval no longer reads its older, wider-spaced history as one long gap. A_deltabucket with no sample of its own reports zero only where an accepted sample interval spans it: through a collection outage, and before a probe's first sample or after its last, it isnulllike every other derived form, rather than a run of confident zero bars. (#402) -
Scope the top queries leaderboard and the query detail overlay to the dashboard time range. The
GET /api/v1/metrics/top-queriesendpoint had no time dimension and always reported the latest collected sample; it now acceptstime_range, defaulting to1h, along withtime_startandtime_endfor a custom window, resolved by the same code asGET /api/v1/metrics/queryso that the rejection messages match every other windowed endpoint. Becausepg_stat_statementsreports its counters cumulatively, the endpoint sums the differences between consecutive samples in the window rather than reading one sample:calls,total_exec_time,rows,shared_blks_hitandshared_blks_readnow cover the window,mean_exec_timeis derived from those sums, and a sample pair whose counters went backwards is discarded as apg_stat_statements_reset(). Themin_exec_timeandmax_exec_timefields stay lifetime figures, because a window extreme cannot be recovered by differencing two lifetime extremes, so ordering on either one sorts a windowed list on a lifetime figure. A statement that ran no calls inside the window is left out of the response altogether, which is a change in what the existing presets return and not only in what a custom window returns. On the client, both the leaderboard and the overlay follow the dashboard selector, including a custom window, and changing the range returns the leaderboard to its first page, since a shorter window usually matches fewer rows. The overlay's Total Calls, Total Time and Mean Time tiles now cover the selected window and name it in their labels, such as "Mean Time (Last 24h)" or "Mean Time (Custom Range)", whilst "Min Time (All Time)" and "Max Time (All Time)" report their lifetime figures. (#387) -
Speed up the top queries list with a new index on
metrics.pg_stat_statements, added by collector schema migration 15 (#387). The index costs roughly 17% more storage on that table and about 56% more write-ahead log volume on the collector's writes, and building it blocks the collector's inserts whilst it runs, which took about 15 seconds for 2.6 million rows in testing. -
Stop the server's "send test notification" path using the
HTTP_PROXYandHTTPS_PROXYenvironment variables. A test send to a webhook, Slack, Mattermost or Telegram channel now connects to the provider directly, as the alerter's deliveries always have, so that the server can check the address it connects to. A server that reaches a provider only through a proxy now fails the test. See Test Sends.
Fixed
-
Make an unbracketed IPv6 SMTP host, such as
2001:db8::25, work in an email channel. The alerter previously joined the host and port with a bare colon, so every delivery to such a host failed with a "too many colons" error. A host entered in square brackets, such as[2001:db8::25], still works. -
Make the server's test send for an email channel behave as the alerter's delivery does. An SMTP server that accepts the connection and then stops answering now fails the test after 30 seconds; release 1.0.0 limited only the connection attempt, so such a server could hold the request open indefinitely. With TLS turned off, the test send now upgrades the connection with STARTTLS whenever the SMTP server advertises the extension, as the alerter does, instead of always sending in plain text, so a test no longer passes or fails differently from a real delivery for that reason. The test send also accepts an SMTP host entered as an IPv6 address in square brackets, such as
[2001:db8::25], which it previously refused as an invalid SMTP host. -
Connect the collector to a monitored connection's
hostaddrwhen one is set, as the server already did. The collector previously senthostaddras an unsupported parameter and left outhost, so the driver connected to its default host (PGHOSTor the local socket), and such a connection generally failed. -
Keep a literal
+in a query parameter value ofPGEDGE_POSTGRES_CONNECTION_STRING, as libpq does, instead of turning it into a space; write a space as%20. -
Fix SQL generated by Ask Ellie and the chart analysis failing on PostgreSQL 17 and later, where the checkpoint columns of
pg_stat_bgwritermoved topg_stat_checkpointerandbuffers_backendwas removed. (#286, #292) -
Match the
metric_namegiven to theget_metric_baselinesMCP tool partially and without regard to case, and list the available metric names when nothing matches. (#287) -
Fall back to the default server secret location in the alerter when
secret_fileis unset, as the collector and server already did. (#291) -
Show the admin privileges a user inherits through a group on the Users tab of the Administration console. (#296)
-
Stop showing a database that was dropped during the selected time range as a card in the server dashboard's Database Summaries. (#363)
-
Use Gemini embeddings for knowledgebase search when
embedding_providerisgemini. (#337) -
Build a Gemini embedding provider in the alerter, configured with the new
llm.gemini.embedding_modeloption, which defaults togemini-embedding-001, and replace the retired Gemini embedding models in the defaults. (#284, #285) -
Populate Top Queries when
pg_stat_statementsis installed in a database other than the connection's, as is common on Amazon RDS. The collector now detects extensions in each database, logs a failed probe as an error, and skips databases it may not connect to, such asrdsadmin. (#435, #438, #440) -
Roll back a failed metrics insert in the collector rather than returning its connection to the pool inside an aborted transaction. (#424)
-
Label chart y-axis ticks distinctly when the value range is small, and show tooltip values at a useful precision (#803). Every fractional value was rounded to one decimal place, so ticks at a step of 0.05 repeated (0.3, 0.3, 0.2) and a sample of 0.04 showed as 0.0; the K, M and B abbreviations had the same problem, so 1.25K and 1.3K both read 1.3K. Axis ticks now keep the digits of the step the chart chose, and tooltips show two decimal places, or three significant figures for values below one; whole numbers lose the trailing zero, so 2.0K now reads 2K.
-
Stop Ask Ellie and the AI analyses from inspecting the wrong database (#800, #801). Asked to list the databases on a connection, Ellie sometimes inspected the schema of one database instead and reported its tables and views as the server's databases; a server analysis could inspect the empty
postgresdatabase rather than the connection's configured database, then report the server as misconfigured. Theget_schema_infotool now names the database it inspected at the top of its result, states that the objects listed are inside that database, and flags adatabase_nameoverride that replaced the configured database; when an overridden database is empty, the result gives the override as the reason rather than a wrong connection or missing permissions. The recovery hints inget_schema_infoandsimilarity_searchnow name thepg://system_inforesource correctly. The tool descriptions and Ellie's instructions now say how to list the databases on a server, and the server and cluster analyses now tell the model each server's configured database and that an emptypostgresdatabase is normal. -
Include alert activity in the recent events the AI Overview reports (#807). The overview considered only restarts, configuration changes and blackouts, so an estate or server whose Event Timeline showed alerts firing and clearing was described as having no recent events. The overview now draws on every event type the timeline shows, including alert, HBA, ident and extension events, and reports the full count of events in the last 24 hours rather than the sample it passes to the LLM. Restarts are sampled on their own, so a restart still reaches the overview however many other events surround it. The timeline also no longer reports an ident mappings change for a stored
pg_ident.confsnapshot whose mappings match the previous one. The estate summary is now also regenerated when new events have occurred and the current summary is at least five minutes old, so alert churn that leaves the active alert counts unchanged no longer leaves the summary out of date. -
Label every server in the Comparative Metrics bar charts on the Cluster Dashboard (#804). The charts reused the time-series axis, which hides labels that collide, so only alternate bars were labelled, and a server name that the browser could read as a date, such as
replica-12, was shown as a time. Bar charts of named items now label every bar, shortening a long name in the middle so that different servers keep different labels, even when they share a long prefix (production-db-node-1andproduction-db-node-2) or differ only in the middle (cluster-a-node-01andcluster-b-node-01), and rotating the labels when there are more than three; the tooltip shows the full server name. -
Offer the Cluster scope in the Create Blackout and Schedule Blackout dialogs for auto-detected clusters and for the servers in them (#809). The option was disabled unless the cluster had been created by hand, although the alerter already suppresses alerts for a cluster blackout on an auto-detected cluster. The cluster list returned by
GET /api/v1/clustersnow includes adb_cluster_idfield holding the numeric ID to send ascluster_id. The navigator's blackout indicator now shows cluster blackouts on auto-detected clusters, and the blackout banner for a selected cluster, or for a server in it, now lists its cluster blackouts, which it never matched before. This holds for a cluster selected from its card on the Estate Dashboard as well as from the navigator, and a selected cluster follows its servers to a new cluster record after a switchover; a selection that no longer matches any cluster offers no Cluster scope. Standalone servers still offer no Cluster scope. -
Colour the Dead Ratio column of the Database Dashboard's Vacuum Status section by the ratio itself, and leave system catalogs and TOAST tables out of the list (#806). The column took its colour from how long ago the table was last vacuumed, so a table with a dead ratio above 90% showed green when autovacuum had run recently. The ratio is now green up to 5%, amber up to 20% and red above that, as on the Server Dashboard's database cards, and the list shows only the tables that
pg_stat_user_tableswould. The section now lists the 20 tables with the highest dead ratio rather than the 20 with the most dead tuples, so a small table with a high ratio is no longer left out;GET /api/v1/metrics/latestacceptsorder_by=dead_tuple_ratiofor this on probes withn_live_tupandn_dead_tupcolumns. -
Stop the AI Overview panel waiting on its loading placeholder until the page is reloaded (#808). The overview stream now delivers a cached or newly generated summary for the selected server or cluster straight to the browser that asked for it, or an error notice when generation fails. Every stream opens with the cached summary or, when none is ready, a generating notice (or, on the estate-wide stream, an error notice once its first refresh has failed), and a stream for a user who can see no connections opens with an empty notice. The panel shows a generic message when generation fails, with the details in the server log, and fetches the overview once over the REST API if nothing has arrived five seconds after the selection changes. The
GET /api/v1/overviewendpoint now returnsstatus: errorwith anerrormessage, rather thanstatus: generating, when the first estate overview could not be generated. -
Stop a scoped AI Overview revealing resources the user cannot see. Both overview endpoints now return
400 Bad Requestwhenconnection_idsis combined withscope_typeorscope_id. A scope's display name is looked up only after the user's access to the scope has been confirmed, and thescope_nameparameter is used only withconnection_ids; it is trimmed, stripped of quotes and control characters, capped at 128 characters and kept apart in the cache, so two users naming the same connections differently never share a summary. The blackouts summarised for a server or cluster overview now include only those that apply to its connections, rather than every active blackout in the estate. -
Apply probe configuration changes to servers that are already monitored (#810, #811). A change in Probe Defaults, or to a cluster or group override, now reaches every server without an override of its own at the Collector's next configuration reload, where before it reached only servers added afterwards. Disabling a probe, at any level, now stops its collection, where before the Collector went on running it, including after a restart. The
PUT /api/v1/probe-configs/{id}endpoint now marks a server, cluster, or group setting it changes as an override, and the datastore migration treats a server setting changed through that endpoint before the upgrade as an override too, so it keeps its value. A server setting changed directly in SQL without settinguser_modifiedis not detected, and follows its parent after the upgrade; setuser_modified = TRUEon such a row, or re-create the override in the Probe Configuration tab. The Alerter'smetric_stalenessrule now reads whether a probe is enabled, and its interval, from the server's own setting as the Collector does, so a probe disabled on one server no longer raises a false staleness alert there, and a probe enabled on one server against a disabled default is checked for stalls. -
Report a statement that runs in several databases once for each database in the Top Queries list (#802). A statement such as
BEGINhas the same query identifier in every database, and the unfiltered list showed it on a single row that added up the calls of every database and named only one of them, usually the Workbench's own datastore. Each database now has a row of its own with only its own figures, and the query detail overlay opened from a row shows the figures for that row's database, in its charts as well as its summary.GET /api/v1/metrics/querytakes a newstatement_database_nameparameter for pg_stat_statements, which keeps only the statements that ran in the named database. -
Show the PostgreSQL version, operating system, and Spock version on the server information card in the status panel whenever they are known (#805). The card read each value only from a sample written in the last five minutes, or the last hour for the Spock version, but the probes behind them run hourly or every ten minutes and the server and extension probes store a sample only when a value changes, so the fields were usually missing; the card now shows the newest stored value, as the server information dialog does, and shows the Spock version for members of manual clusters and for cluster members that the latest detection pass did not find. The Spock version comes from the newest set of extensions stored for the server, so a server that no longer has Spock shows no version; where Spock is installed in more than one database, the card shows the version in the connection's own database, or otherwise the highest. The same fix restores the match between a binary standby and its primary by system identifier. The collector also now keeps the newest
pg_extensionsnapshot for each server past its 30-day retention, so the Spock version is not lost on a server whose extensions do not change. -
Keep the user list on the Administration > Users page when an action on a row is refused (#788). A failed Superuser or Enabled toggle, or a failed delete, replaced the whole page with the error until the panel was closed and reopened; the error now appears in a dismissable message above the list. A user who is not a superuser, and so cannot change the superuser flag, now sees the Superuser switch in the list disabled and no Superuser switch in the Create User and Edit User dialogs.
-
Return the stored baseline rows from
GET /api/v1/metrics/baselines(#348). The endpoint averaged every baseline row of a metric across its periods, and filtered on a period type that the alerter never writes, so the figures it returned were not any baseline the alerter used. It now returns one row for each metric, database and period, with newperiod_type,database_name,hour_of_day,day_of_weekandearliest_sample_atfields; theperiod_typeparameter selectsall(the default),hourlyordaily. -
Apply the custom LLM headers set in the server configuration file (#754). The
llm.custom_headers,llm.custom_headers_filesand fourllm.<provider>_custom_headersoptions were read from the file and then discarded, without a warning, so no configured header reached the LLM provider; only theLLM_CUSTOM_HEADERSandLLM_<PROVIDER>_CUSTOM_HEADERSenvironment variables worked. The options are now applied, and documented in the server configuration reference. -
List a server's databases when one of them is dropped whilst the list is being built (#723). The size lookup for the dropped database returned NULL, or failed because the database no longer existed, and either failed the whole request; a database dropped part way through is now left out of the list, and the listing is retried, up to three times in all, when the drop has already completed.
-
Accept API tokens when creating, updating or deleting a connection or a cluster group (#668). These endpoints looked the caller up as a browser session, so a token the server had already authenticated was answered with
401 Unauthorized; a token now acts as its owner, so a connection or cluster group it creates is owned by the token's owner. The token's administrative and connection scopes still apply, and creating a connection, like any cluster group write, needs a connection scope covering every connection atread_write. -
Delete a connection with a long metrics history without timing out (#669). The delete removed the connection's metrics in the request through
ON DELETE CASCADE, which could take longer than the request deadline, so the server answered500and the connection stayed in place and monitored. The server now marks the connection deleted and answers204straight away; from then on the connection is hidden from every listing, the API and the MCP tools, even for a superuser, and the collector and alerter stop monitoring and alerting on it. The server then removes its metrics in the background, in bounded batches, and finally the connection row, retrying a failed batch after 15 seconds (backing off to five minutes if it keeps failing) and resuming after a restart. Collector schema migration 29 adds thedeleted_atcolumn the mark uses. -
Answer
404 Not Foundwhen updating or deleting a connection the caller cannot see, exactly as for a missing one and after the same database queries, so neither the answer nor its timing reveals whether the connection exists; a caller who can see the connection but may not change it still gets403 Forbidden(part of #677). An update whose connection is deleted whilst the request is in flight also answers404rather than500. -
Fix Ask Ellie tool calls failing with the OpenAI provider (#686). The web client returns tool results in a user message, which the OpenAI provider dropped, so OpenAI rejected the next request with a 400 error saying the tool calls had no responses. The server now sends tool results to every provider as tool messages.
-
Store
pg_hba_file_rulessnapshots on PostgreSQL 16 and later whenpg_hba.confcontains a line PostgreSQL cannot parse (#680). PostgreSQL reports no rule number for such a line, so the insert of the whole snapshot failed and the last good snapshot stayed as the latest, with no alert raised. The collector now stores the line with a negativerule_number(-1, -2 and so on, ordered by file name and then line number), alongside the valid rules, which keep their own numbers. -
Load every configured alerter API key file when one of them cannot be read (#626). The alerter previously stopped at the first unreadable file, so a bad OpenAI key file also left the Anthropic, Voyage, Gemini and TypeSafe keys unloaded; it now loads the rest and logs one warning per file that failed.
-
Fix the anomaly detection baselines for
pg_stat_database.deadlocks_deltaandpg_stat_database.temp_files_delta, which counted one more sample-to-sample increase in each hour than the live value does, because each hour's first sample was compared with the previous hour's last (#639). The baseline sat too high, so a server doing the same work every hour scored as unusually quiet; each hour is now counted on its own, as the live value is. -
Fix the
pg_stat_statements.slow_query_countalert metric reporting the whole server's count once for every database (#640).pg_stat_statementsis cluster-wide, and the collector stores its rows once for each database it probes, so every database reported the same figure and the Slow Query Count rule raised one alert per database for a single problem. The metric is now reported once per server, counting each statement once whichever database it ran in; statements from databases the collector does not probe, or that lack the extension, still count. The alerter closes active alerts on the metric that name a database, without sending a clear notification, and raises a single server alert if the condition still holds; acknowledged ones are left for retention to delete, and baselines stored per database are removed by the next baseline cycle. -
Fix the
list_probesMCP tool, which hid every probe whose name contained a 'p' after its first character (such aspg_stat_replication) whilst listing the collector's weekly partitions as though they were probes (#644). The tool filtered partitions by name with aLIKEpattern in which_matched any character; it now leaves out partitions by checking the system catalogue, so it lists each probe table, including partitioned parents, and no partitions. The row count for a partitioned probe is now the total across all of its partitions, where it would otherwise always have been zero. -
Fix the
pg_stat_replication.replay_lag_secondsalert metric, which never returned a value because its query read areplay_lsn_timestampcolumn that the collector does not store (#625). The metric now reports, for each connection, the largestreplay_lagacross the standbys in that connection's most recent replication sample, which is the replay lag PostgreSQL itself reports. It reports nothing once PostgreSQL resetsreplay_lagto NULL for an idle standby that has caught up, so an alert on the metric clears rather than lingering. The alert rule reference no longer lists a built-in time-based replication lag rule, because none has ever been seeded; operators can create a custom rule on the metric instead. -
Send alert re-evaluation requests to the LLM with a system prompt that asks whether to clear or keep the alert. The re-evaluation worker sent the tier 3 anomaly system prompt, which asks for an alert or suppress decision, so a model that followed it gave an answer the alerter could not parse; the alerter then kept the alert unless the reasoning happened to contain a phrase such as "should be cleared", and asked the LLM the same question again on the next cycle. The reasoning provider health alert also counted each such answer as a failure. Because the system prompt is part of the re-evaluation fingerprint, each acknowledged anomaly alert is re-evaluated once more after upgrading. (#627)
-
Show the start of a long Ask Ellie reply rather than its end (#629). The chat panel scrolled to the bottom whenever a message arrived, so a reply taller than the panel had to be scrolled back up before it could be read. A reply that does not fit now scrolls so that its top sits at the top of the message list; a message the user sends, or a reply that fits, still scrolls to the bottom, and a reply arriving whilst the user has scrolled up to read earlier messages leaves the view where it is.
-
Use the leftmost
X-Forwarded-Forentry from a trusted proxy as the client address only when it parses as an IP address, falling back to the proxy's own address otherwise, so arbitrary header text no longer reaches the login rate limiter. A forwarded address that carries a port, as some proxies send it, is now read without the port rather than skipped. (#589) -
Refuse a
connection_idthat is not a positive integer, such as3.7,2^63or the string"5", in every MCP tool that takes one, rather than truncating it to the connection it would have named or ignoring it in favour of the selected connection. Anullconnection_idis treated as if it had been omitted. (#706) -
Require the
store_system_memorypermission to pin or unpin a system-scoped memory throughPATCH /api/v1/memories/{id}; previously any signed-in user could change whether a system memory was injected into every user's assistant context. The refusal answers 403 withPermission denied: requires store_system_memory permission. Another user's memory still answers 404, as a missing one does, and no longer runs a delete. The MCPdelete_memorytool previously let the user who created a system memory delete it withoutstore_system_memory; it now requires the permission as the REST route does. (#704) -
Clear an active anomaly alert once its metric recovers. The alerter cleared only threshold alerts automatically, so an anomaly alert stayed open after its metric returned to normal until an operator dealt with it. Each tier 1 evaluation now re-scores the metric behind every active anomaly alert against the same baseline tier 1 uses, and clears the alert, sending the usual clear notification, once the number of consecutive samples set by the new
anomaly.tier1.clear_countoption (default 3, minimum 1) have scored back inside the sensitivity band. Only a newly collected sample counts, so evaluations that re-score a sample already counted neither advance nor restart the count. Whilst the value stays out of band, the alert's metric value, anomaly score and last-updated time are refreshed and its severity can rise but never fall; the description keeps the original values. An evaluation in which the metric does not report holds the alert open and keeps the count; an out-of-band sample, an unusable baseline or a blackout holds it open and restarts the count, which is kept in memory and also restarts when the alerter does. For metrics that report only whilst their condition holds, such as blocked sessions, each new collection that reports no value counts as an in-band sample, provided the probe is still collecting for the connection. On upgrade, active anomaly alerts that can no longer be re-scored (those on a metric anomaly detection no longer covers, and those with no database on a per-database metric, as earlier releases raised for the cache hit ratio, deadlock and temporary file metrics) are closed without a clear notification, and their description records why. Acknowledged anomaly alerts are left to re-evaluation, and no new anomaly alert is raised on the same metric, connection and database within five minutes of one clearing. Recovery uses tier 1 only and makes no embedding or LLM call. (#611) -
Stop the alerter's retention sweep from blocking vacuum across the datastore. The sweep deleted every aged anomaly candidate, and with it each candidate's embedding, in a single transaction; on a large installation that ran for over 23 minutes and held back the xmin horizon, so vacuum could clean nothing else in the datastore whilst it ran. The alerter now deletes aged anomaly candidates and aged alerts in batches of 1,000 rows, each in its own short transaction, and stops between batches when it is shut down. Collector schema migration 20 adds the indexes the batched delete and its cascades need on
anomaly_candidates(processed_at,embedding_idandalert_id), and dropsidx_anomaly_embeddings_candidate, which duplicated the unique index onanomaly_embeddings.candidate_id. The new indexes are built withoutCONCURRENTLY, so the collector blocks writes toanomaly_candidateswhilst they build when it first starts after the upgrade. Stop the alerter before upgrading the collector: dropping the duplicate index waits for any retention sweep the old alerter is still running, and blocks every reader ofanomaly_embeddingsuntil it can proceed. (#615) -
Stop tier 1 anomaly detection raising alerts for values that are unusual for a quiet baseline but trivial in absolute terms, such as 12 temporary files raised as critical against a baseline that had never moved, CPU usage of 17 to 37%, or a single session on an idle server. Each standard metric now has a built-in
min_value, below which the metric is never anomalous, and amin_stddev, the smallest divisor the z-score uses in the metric's units; both can be overridden per metric under the newanomaly.tier1.metric_floorsoption. A value undermin_valuecounts as in band when an anomaly alert is cleared, and an alert scored against a baseline with no spread of its own is raised as a warning at most. The alerter logs a warning at startup and on reload for anymetric_floorsentry naming a metric that anomaly detection does not score. (#617) -
Apply the datastore password read from the server's
-db-password-fileflag. The file was read but its contents were then dropped, so the server connected with the configuration file's password, or none; the file's password now overrides the configuration file at start-up and on aSIGHUPreload, as-db-passworddoes, and-db-passwordstill takes precedence when both are given. The datastore password is also now excluded when the server's configuration is serialised to JSON, as the OIDC client secret already was. (#591) -
Fix
GET /api/v1/metrics/latestreturningNUMERICcolumns, such as the replication slotretained_bytes, as unreadable strings like{123456789 0 false finite true}; they are now JSON numbers, ornullwhen the value is not finite. The endpoint also no longer treats a replication slot'swal_statusas part of the slot's identity, so a slot whose status changed within the hour is reported once with its current status rather than once for each status. (#347) -
Keep the server's command-line overrides across a
SIGHUPconfiguration reload. A reload applied none of the flags given at start-up, so the-db-*flags were replaced by the configuration file's database settings, and a server started with-tlswas validated as though TLS were off. A reload now applies every flag given on the command line, as start-up does. (#506) -
Fix the alerter's re-evaluation worker sending each acknowledged anomaly alert back to the LLM every time it fell due, by default every five minutes, although nothing the prompt shows had changed since the last "keep" answer. When the LLM answers "keep", the alerter now stores a hash of the prompt inputs (the alert's value and baseline, its acknowledgement, past acknowledgements of the metric, the other alerts on the server, the cluster context, the reasoning provider and model and the system prompt, but not the re-evaluation count) in the new
alerts.reevaluation_fingerprintcolumn, and skips the call whilst the hash is unchanged. A new acknowledgement, a change in the server's other alerts, a new provider or model, or a reworded system prompt sends the alert to the LLM again. A failed call, a response that contains no decision, or one made when part of the context could not be read stores no hash, so the alert is retried when next due. The collector adds the column in schema migration 18, so upgrade the collector before the alerter. (#575) -
Fix a connection created without a description breaking the connection list for every user. The server stored the missing description as
NULLand then failed to read the row back, so the create request returned an error although the connection had been saved, and every later request to list connections failed until the row was corrected by hand. The server now stores an empty description and reads an existingNULLas an empty one, and collector schema migration 17 replaces anyNULLdescription with an empty string and makes the columnNOT NULL. (#540) -
Exclude
pg_settings.max_connectionsandpg_replication_slots.inactive_countfrom the alerter's anomaly baselines and anomaly detection. The first is a configuration value that exists for thehigh_max_connectionsthreshold rule, and its baseline was built from a single sample, so it could never pass warm-up; the second is a presence count that is almost always zero, and thereplication_slot_inactivethreshold rule onpg_replication_slots.inactivealready alerts on the condition it counts. Threshold rules that read either metric are unchanged, the alerter deletes their existing baseline rows on its next baseline cycle, and it suppresses any anomaly candidate for them recorded before the upgrade rather than raising an alert from it. (#576) -
Fix the database summaries reporting a failed query as an empty success. Each sub-query logged its error and returned no rows, so a statement timeout or a broken metrics table produced a blank or partial summary with status 200. The endpoint now returns status 500 for any failure other than a missing metrics table, which still returns an empty list. A row holding a NULL database name, size or connection count is now skipped rather than ending the scan early and hiding every database after it. (#519)
-
Fix the Table Leaderboard and Index Leaderboard on the database dashboard listing
pg_catalogandinformation_schemaobjects, each shown with zero rows or zero reads. Both leaderboards ranked every schema the collector records, and PostgreSQL reports zero counters for a catalog table it holds no statistics for, so on a database where every counter is zero, such as a standby or a server whose statistics were reset, catalog objects filled the top ten. Both leaderboards now leave out the system schemas that PostgreSQL leaves out ofpg_stat_user_tablesandpg_stat_user_indexes:pg_catalog,information_schemaand the TOAST schemas. The/api/v1/metrics/latestendpoint gains anexclude_system_schemasquery parameter for this purpose, and its existingexclude_schemasparameter is now documented in the API reference. The Vacuum Status section still includes the system schemas. (#499) -
Fix the collector opening more connections to a monitored server than
pool.max_connections_per_serverallows. The limit counted only connections in use, whilst each database on the server had its own pool that kept its connections open formonitored_max_idle_seconds, so a server with many databases held an idle connection for each of them. The limit now counts every connection the collector has open to the server, idle or in use, and idle connections to other databases are closed before a new one is opened. Every configuration reload also closed and reopened all of a server's connections even when nothing had changed, because the hash that detects a changed connection was computed over parameters in random order; the hash is now stable. (#539) -
Fix the server ignoring
llm.temperature: 0. The configuration merge treated a temperature of 0 as unset, so the 0.7 default won and an operator could not ask for deterministic output. An explicit 0 is now passed to the provider, whilst an omitted or negative value still falls back to 0.7. (#551) -
Fix the alerter paying for Tier 2 and Tier 3 anomaly analysis on candidates that could never raise an alert. Every candidate went through an embedding call and an LLM call before the alerter checked for an active blackout, an open anomaly alert on the same metric, connection and database, or re-evaluation and false-positive suppression, so a persistent condition bought both calls on every evaluation cycle only for the result to be discarded. Those checks now run before Tier 2, and a candidate that matches one is marked as processed without either call, linked to the open alert where there is one. The checks run again just before an alert is created, since a blackout or an acknowledgement can arrive during a slow Tier 3 call. (#568)
-
Fix the alerter writing an anomaly candidate on every evaluation cycle for a condition that could not raise an alert. Tier 1 stored a candidate for each anomalous value even whilst an open anomaly alert, a database-scoped blackout or a suppression applied, so a persistent condition added a row per metric, connection and database every cycle, each later marked as processed and deleted. Tier 1 now runs the pre-tier checks first and stores no candidate when one applies. (#577)
-
Fix anomaly candidates accumulating for ever after a configuration reload that left no tier after Tier 1 able to process them. At startup the alerter already disables anomaly detection when no Tier 2 or Tier 3 provider is available, because Tier 1 alone is too noisy to alert on, but a reload did not repeat the check: the running detector kept storing Tier 1 candidates that nothing processed, and the retention cleanup only deletes processed candidates. A reload now applies the startup rule, Tier 1 stores no candidate unless Tier 2 or Tier 3 can process it, a processing run under way when the reload lands stops rather than alerting on the rest of its batch, and a candidate left unprocessed for longer than three times 100 Tier 3 timeouts (two and a half hours by default, and never less than one hour) is marked as processed with no final decision and counted in the log, so the backlog raises no stale alerts and ages out through the normal retention cleanup. (#581)
-
Fix the alerter's anomaly baselines for
connection_utilization_percentand thepg_stat_activity.blocked_count,idle_in_transaction_seconds,max_query_duration_secondsandmax_xact_duration_secondsmetrics counting background processes such as autovacuum workers, walsenders and the checkpointer, whilst the live values counted client backends only. The skewed baselines produced a steady stream of spurious anomaly candidates, each of which could incur paid Tier 2 and Tier 3 calls. The baselines now count client backends only, and theconnection_utilization_percentbaseline divides each sample by themax_connectionsvalue in force when that sample was collected rather than by the current value. Each baseline cycle now also deletes the rows of a metric that it did not rewrite, such as an hourly bucket that no longer has enough samples, so rows built from the old values do not survive the upgrade. (#567) -
Fix the alerter's live
connection_utilization_percentvalue dividing the newest activity sample by the newestmax_connectionssetting even when that setting was recorded after the sample, for example just after a restart that changedmax_connections. The live value now uses the setting in force when the sample was collected, as the baseline does, so the two agree for the same snapshot. (#596) -
Fix alerts staying active for ever on a server an operator has stopped monitoring. The probes of an unmonitored connection leave the staleness view, so every alert on it was judged to have a probe that is not reporting and was held open, whilst retention only reaps a cleared or acknowledged alert, so nothing removed them and nothing said why they were stuck. The alerter's cleanup pass now clears every active alert on a connection whose monitoring has been turned off, whatever the type of the alert, and rewrites its description to record that it was closed because monitoring of the server stopped rather than because the condition ended. No clear notifications are sent for those alerts, since nothing resolved the conditions they report. An alert on a stalled or unavailable probe is still held open as before. Alert retention also reaps acknowledged alerts again: it measured the age of a retired alert by its clear time, which an acknowledged alert never has, so an acknowledged alert was kept for the life of the installation. It now falls back to the time the alert was raised. (#500)
-
Fix the top queries list reporting a query under every monitored database when it is filtered by database. The server named a query's database from the backends the collector happened to sample in the previous hour, and when none had been sampled it used the database the statistics were collected through instead; since those statistics are collected once through every database with
pg_stat_statementsinstalled, a filtered list then showed the same query, with the same calls, under each of them. The name now comes from the per-database statistics the collector records for every database it monitors, so the query appears under the database it actually ran in. A query in a database the collector cannot see still falls back to one of the collecting databases, but only to one, so the filtered and unfiltered lists agree. The query drill-down charts now read their data through the database the statistics were collected in, so they no longer come up empty for a query whose own database lacks the extension, and the/api/v1/metrics/query-statsendpoint resolves the database the same way as the list and no longer counts a query's calls once for every database it was collected through. (#508) -
Fix the AI analysis offering a Run button on generated SQL that could not be executed, so that a query naming a hallucinated column, a mistyped catalog reference or a role that does not exist failed only once the user had run it. Every SQL block in an analysis report is now checked as the report renders, through a new read-only endpoint,
POST /api/v1/connections/{id}/query/validate, that plans each statement withEXPLAINinside a read-only transaction and rolls that transaction back; the endpoint never executes the statement itself. Run is unavailable whilst the check is in flight. A statement PostgreSQL rejects shows the error beneath the block and keeps Run disabled behind a Run anyway button, and a statement outside the kinds the check plans, which areSELECT,WITH,INSERT,UPDATE,DELETE,MERGE,VALUES,TABLEandEXPLAINwithoutANALYZE, stays runnable under a "Not validated" notice giving the reason;EXPLAIN ANALYZE, the DDL statements,ALTER SYSTEM,VACUUM,SET,SHOW,GRANT,REINDEX,CLUSTERandANALYZEall fall into that group. A block carrying$1-style parameter placeholders is labelled a query template and offers no Run button at all, because such a query cannot run as written. The analysis prompts now require the model to callget_schema_infobefore naming a user object or a catalog column outside the built-in crib sheet, and forbid parameter placeholders in runnable blocks; alert analysis gained theget_schema_infotool, which it previously lacked. Where a finished report still contains a statement that fails validation, the analysis runs one repair round, feeding the validation errors back to the model before the report is shown. (#532) -
Fix the
test_queryMCP tool rejecting valid parameterised queries and reporting every statement of a multiple-statement query as invalid. The tool now plans a parameterised query withEXPLAIN (GENERIC_PLAN)on PostgreSQL 16 and later, rather than failing it for the missing parameter values, and it rolls the transaction back before retrying each statement individually, so that the per-statement results are no longer produced by an already aborted transaction. (#532) -
Fix the transaction throughput chart on the Performance Summary page counting a database's whole lifetime transaction count into one interval. The commits and rollbacks per second were derived by differencing the cluster-wide sum of the
pg_stat_databasecounters, so a database created between two collector samples added its entire counter to that interval as a spike, whilst a database dropped between two samples turned the difference negative and flattened the interval to zero. The counters are now differenced for each database separately and only then summed, and an interval is discarded for a single database when its statistics were reset, when it was missing from the preceding sample, or when the sample has no predecessor in the selected range. Charts over a window in which databases were created or dropped therefore report lower, accurate rates where they previously showed a spike or a gap. (#469) -
Fix six of the nineteen
builtins.toolsswitches on the server having no effect, because the configuration merge read their values from the configuration file and then discarded them: settinglist_connections,get_alert_history,get_alert_rules,get_metric_baselines,query_datastoreorget_timeline_eventstofalseleft the tool enabled. That mattered most forquery_datastore, which then ran arbitrary read-only SQL against the datastore and could not be switched off at all. Every switch now takes effect. Theget_blackoutstool, which the documentation and the example configuration both advertised as switchable, had no backing configuration field at all; the tool now has one and behaves like the rest. Thetest_querytool was advertised the same way, and is now permanently enabled alongsideread_resourceinstead: the tool only validates SQL without executing the statement, and the server asks Ellie to check every statement through that tool before showing the statement to a user, so an operator who disabled the tool would leave the assistant calling a tool that is not registered. An existingtest_query: truesetting is harmless, but the key is no longer recognised and has been removed from the documented example and fromexamples/ai-dba-server.yaml. (#521) -
Fix the
pg_stat_statementscollector probe discarding the block timing columns on every modern server. The probe chose its query shape by looking for the version-specific columns ininformation_schema.columnswithtable_schema = 'pg_catalog', butCREATE EXTENSIONinstalls the view into the current schema, normallypublic, so both checks failed and the probe fell back to the PostgreSQL 12 query. Rows arrived withshared_blk_read_time,shared_blk_write_time,local_blk_read_timeandlocal_blk_write_timestored as NULL andtoplevelfixed atTRUE. The checks now resolve the view through the search path, exactly as the probe's own query does, so a relocated extension is detected in whichever schema it was installed, provided that schema is on the collector role's search path. (#439) -
Fix Ask Ellie failing with
Function call is missing a thought_signature in functionCall partson every question that needs a tool call when the LLM provider is Gemini. The server now echoes the signature Gemini attaches to each function call back on the next request, reports tool failures to Gemini instead of letting the model retry the identical call, and hides Gemini models that cannot hold a text conversation from the model list. (#425) -
Fix every cache hit ratio in the web client (the Cache Hit performance tile, the server dashboard's KPI sparkline and per-database cards, the database dashboard's KPI tile and Cache Hit Ratio Over Time chart, and the cluster dashboard's comparative chart) reporting a lifetime average since the last statistics reset, which could not move when a current problem appeared. The ratio is now computed from the change in
blks_hitandblks_readbetween consecutive samples within each time bucket. Intervals with no block access render as a gap in the chart and '--' for the headline value instead of 0%, samples following a statistics reset are discarded rather than producing a bogus ratio, and the tooltips note thatblks_hitcounts onlyshared_buffershits, so a lower ratio does not by itself indicate slow I/O. (#401) -
Enforce the
required_extensionfield on alert rules. The alerter stored the field but never checked it, so rules that depend onpg_stat_statements,system_statsor Spock were evaluated against every connection. The alerter now reads the newestmetrics.pg_extensionsnapshot for each connection and skips connections that lack the extension. An existing alert on a connection that lacks the extension is left active rather than resolved, so uninstalling an extension no longer reports the underlying condition as cleared. (#409) -
Fix the
metric_stalenessalert rule firing and clearing in a loop, which sent a notification pair every cycle for as long as a probe remained stale. The alert cleaner resolved the rule's metric through the metric registry, where the bespoke staleness metric has no entry, and treated the resulting lookup error as proof that the condition had gone away. The cleaner now distinguishes a metric it cannot evaluate, or a query that failed, from a metric that ran and reported nothing; only the latter clears an alert. Staleness alerts resolve through their own check against probe availability, the staleness evaluator applies the same cooldown guard as every other rule, and each stale probe on a connection now raises its own alert rather than overwriting a shared one. (#405) -
Fix five built-in alert rules that could never fire. The
wal_archive_failedrule queriedmetrics.pg_stat_archiver, a table the collector never creates, and now reads the archiver columns ofmetrics.pg_stat_wal. Thetransaction_wraparoundrule evaluated a hardcoded 50.0 and now reports the transaction ID age of the oldest database as a percentage of the wraparound limit, template databases included;template0does not allow connections, so autovacuum only reaches it on the anti-wraparound path, which makes it the database most likely to age while every user database stays fresh, and excluding templates left the rule silent in exactly that case. The dashboard's XID Age tile now counts templates for the same reason. Thehigh_max_connectionsandconnection_utilizationrules required apg_settingssnapshot from the last hour, which a change-tracked probe stops producing on a stable server, and now read the newest stored snapshot. Thecpu_usage_highrule keyed on a Windows-only column that is NULL on Linux and now derives the busy percentage on both platforms. Thecheckpoint_warningrule compared a per-probe-interval delta against an unreachable threshold and now counts requested checkpoints per hour, with a default threshold of 12. Tuned autovacuum settings are also honoured again byautovacuum_not_running, which previously always assumed the shipped defaults. (#406) -
Fix alerts flapping, latching, or clearing falsely when a metric stops reporting. The alert cleaner treated a latest-metric query that returned no row for an alert's connection and database, or no rows at all, as proof that the condition had resolved, so any gap in collection cleared the alert and the next sample raised it again. The metric registry now records, for each metric, whether a missing row means recovery. Metrics that emit a row for every healthy connection leave their alert active until fresh data shows the condition has ended, and the
metric_stalenessrule reports the probe that stopped; metrics that report only whilst the condition holds, or that describe an object an operator can legitimately drop, such as a replication slot or a standby, still clear on absence, but only when the collector probe behind the metric last collected inside the window that metric's query reads. Every metric bounds the age of the samples it reads, so a stopped collector empties a query exactly as a recovered condition does; the cleaner now checks that window first, and a probe that has stalled, that an operator has disabled, or that belongs to a connection which is no longer monitored leaves the alert active until somebody clears or acknowledges it. The check is made against the metric's own window rather than against a multiple of the probe's collection interval, so raisingcollection_interval_seconds, for every connection or for one, no longer leaves the cleaner quiet whilst ordinary collection empties the query. (#407) -
Fix the
slow_query_countrule latching until a manualpg_stat_statements_reset(). The rule counted the queries whosemean_exec_timeexceeded 1000 ms, a lifetime average since the last statistics reset that never decays, so a query that ran slowly once kept the count elevated whether or not it ran again. The count now covers only the queries that ran during the most recent probe interval, using an interval mean derived from the change in total execution time divided by the change in call count. The samples in the window are sorted once rather than twice, which cut the query's execution time by roughly a third on a fixture of 24,000 statement samples. (#407) -
Fix the
cache_hit_ratio_lowrule firing and clearing on identical data. The metric returned every delta row in its fifteen minute window with no ordering, and the threshold evaluator fired when any row violated the threshold whilst the alert cleaner stopped at the first row it saw, so one set of samples could produce a fire and a clear in the same cycle, or latch the alert when the oldest interval was the violating one. The metric now reduces to the newest qualifying delta for each connection and database, so both sides read the same value. (#407) -
Add a fifteen minute freshness cutoff to the
pg_replication_slots.inactive_countandpg_replication_slots.max_retained_bytesmetrics, which previously had none, soreplication_slot_retention_warnandreplication_slot_retention_highno longer keep firing on days-old samples after a collector stops or a connection stops being monitored. The windows onpg_replication_slots.inactiveandpg_node_role.subscription_worker_downwidened from five to fifteen minutes, because five minutes equalled the probe interval and a single late collection emptied the window, clearing the criticalreplication_slot_inactiveandsubscription_worker_downalerts. Unit tests now require every metric in the registry to boundcollected_at, or to be allowlisted with a reason, and require every metric that clears on absent data to look back over at least three of its probe's seeded collection intervals;pg_settings.max_connectionsis the one allowlisted exception, since its change-tracked probe stores nothing whilst the configuration is unchanged. (#407) -
Split the server dashboard "Connections Over Time" chart into a Connections chart and a Sessions Established chart. The old chart plotted
numbackends, a gauge bounded bymax_connections, alongsidesessions, a counter that climbs until the statistics are reset, so the counter set the scale and flattened the backend count into a featureless line. The Connections chart now shows backends against amax_connectionsreference line, and both titles state that the figures cover the monitored database, since thepg_stat_databaseprobe reports oncurrent_database()only. (#403) -
Fix the collector's partition dropper never enforcing
retention_days, which could fill the metrics datastore's disk and take the Workbench down with it. Garbage collection was scheduled five minutes after process start and every 24 hours thereafter, with no record of when it last ran, so retention was coupled to unbroken process uptime; a collector that restarted more often than the startup delay never collected garbage even once, and each restart returned it to the beginning of the five minutes. A reporter saw partitions persist nine days past a seven-day window across more than a hundred restarts, with no dropper activity in the logs at all. The collector now records each completed pass in a newmaintenance_runstable and times the next pass from that record rather than from process start, so an overdue collection runs shortly after startup however many times the process has restarted. This also closes the same gap for rolling deployments and node drains, which failed identically. A failed pass now backs off rather than retrying immediately. (#437) -
Fix the Query Plan panel in the Query Detail view of the web client showing a raw database error, such as
syntax error at or near "VACUUM", for statements that PostgreSQL cannot explain. Becausepg_stat_statementsrecords utility and maintenance statements alongsideSELECTand DML, a Top Queries row can hold text such as a bareVACUUM,ANALYZE, orREINDEX. The panel now reports that query plans are not available for that type of statement, and no longer attempts a request that cannot succeed. Statements that PostgreSQL can plan, including a bareTABLE, are unaffected and still show their plans. Where PostgreSQL accepts a statement but returns no plan, as withREFRESH MATERIALIZED VIEW, the panel now says so plainly instead of showing the database's internal notice. (#368) -
Fix the "Connection Count" bar chart in the cluster dashboard's comparative section, which plotted a hardcoded value of one for every server rather than a real measurement. The performance summary endpoint (
GET /api/v1/metrics/performance-summary) now reports anactive_connectionscount per connection, taken from the sum ofnumbackendsacross the databases in the server's most recentpg_stat_databasesnapshot, and the chart plots that value. (#404) -
Fix both charts on the query drill-down page, "Execution Time Over Time" and "Calls Over Time", aggregating across every statement in the database instead of the query the user selected. The metrics time-series API (
GET /api/v1/metrics/query) now accepts aqueryidfilter, and the query detail page passes the identifier of the selected query so that both series describe that query alone. (#404) -
Fix the collector and the alerter overriding a configuration file value whenever that value happened to match a command-line flag's built-in default, and ignoring a flag that an operator passed explicitly with its default value. Both services decided whether a flag had been supplied by comparing the flag's value against its own default, which cannot tell the two cases apart; for example, a collector configuration file setting
datastore.port: 6000ignored-pg-port 5432on the command line. Both services now detect the flags that were actually present on the command line, so the documented precedence of defaults, then configuration file, then flags holds in every case. (#389) -
Stop a client that disconnects mid-request costing the server, collector or alerter a pooled database connection, and stop a rollback to a hung server holding a pool slot indefinitely; every transaction is now rolled back with its own five-second limit. (#381)
-
Connection-privilege chips in the Admin Groups and Users panels now show the connection name instead of the internal numeric connection id. (#309)
-
Fix the metrics time-series API (
GET /api/v1/metrics/query) silently returning an empty HTTP 200 response, which the web UI rendered as a generic "No data available" message, whenever a monitored server's metric sample contained a NaN or Infinity value. Go's JSON encoder cannot serialize non-finite floats, so the encode failed silently after the response status had already been sent. The query path now treats a non-finite sample the same way it already treats a missing value from an underlying LEFT JOIN gap; the code carries the last known good value forward when one exists and skips the sample when none does, so a single bad reading degrades gracefully instead of blanking the whole chart. A JSON-encoding failure on this path is now logged loudly rather than silently swallowed, so any future occurrence surfaces in the server logs. -
Fix blank overview tiles and a Maintenance Info panel stuck on "Never" in the Table and Index object-detail dashboards of the web client. These panels requested the single most recent row of stats through the
limit,order_by, andorderquery parameters, but the metrics API ignored those parameters and returned time-bucketed chart data instead. The API now honours these parameters and returns the most recent raw stat rows as real column values, validatingorder_byagainst the table's known columns before use. -
Fix the Table Size and Index Size fields rendering permanently as "--" on the Table and Index object-detail dashboards of the web client. No collector probe ever gathered relation size, so the value genuinely never reached the datastore rather than failing at query time as the two prior dashboard fixes did. The
pg_stat_all_tablesprobe now collectstable_sizeviapg_table_size, which covers heap, TOAST, free space map, and visibility map storage whilst excluding indexes. Thepg_stat_all_indexesprobe now collectsindex_sizeviapg_relation_sizefor each index. Both values are stored as raw byte counts that the web client formats for display. Collector schema migration Version 7 adds the two new columns to themetrics.pg_stat_all_tablesandmetrics.pg_stat_all_indexespartitioned parent tables, and PostgreSQL propagates them to every existing and future partition automatically. The web client's existing latest-row request reads the new columns with no further server or client changes. -
Fix the Activity Charts on the Table and Index object-detail dashboards rendering permanently blank, with the messages "No tuple operation data available", "No scan data available", and "No dead tuple data available" appearing even when the monitored server had completely normal, healthy activity. Those charts request computed metrics, including per-second rates such as sequential scans, index scans, and inserted, updated, or deleted rows, together with a dead-tuple percentage. The metrics API understood only literal, raw stored columns and could not compute a rate or a ratio, so it rejected these requests with an error that the web client silently rendered as missing data. The metrics API now computes a per-second rate for any counter column a dashboard requests by name; a request for
seq_scan_per_seccomputes the rate of change of the underlyingseq_scancounter between samples. The API also computes a dead-tuple ratio metric for tables, reusing the rate and ratio calculations already applied elsewhere in the product. (#342) -
Fix embedding storage rejecting non-OpenAI providers with a dimension-mismatch error. The chat memory and anomaly detection embedding columns were hardcoded to
vector(1536), OpenAI's dimension, which broke Gemini (3072), Voyage (1024 or 512), and Ollama (384, 768, or 1024). The columns are nowhalfvec(4000), and the workbench zero-pads every embedding to 4000 dimensions, so embeddings from any supported provider store and compare correctly. This widening introduces a hard ceiling: an embedding model that produces vectors with more than 4000 dimensions is not supported, because 4000 is pgvector's HNSW index limit for thehalfvectype. The workbench rejects such a model with a clear error rather than truncating the vector. (#294) -
Fix the Estate Overview dashboard's KPI tiles (XID Age, Cache Hit Ratio, Transactions, and Checkpoints) and Event Timeline getting stuck on "No data" indefinitely after a brief backend restart or any transient API failure. Recovering required the user to click the sidebar's Refresh button or reload the page, because nothing retried on its own. The client uses no React Query or SWR; every panel hook refetched only when a single shared
lastRefreshtimestamp changed, and that timestamp advanced only when the sidebar'sGET /api/v1/clusterscall succeeded. A transient failure in an individual panel's fetch, or in the shared refresh call itself, left the affected panels blank with no automatic recovery. A shareduseRetryingFetchhook now provides capped exponential backoff (3s, 6s, 12s, 24s, capped at 45s) for the Event Timeline, the performance-summary tiles, the Estate KPI tiles, and the alerts panel. A manual refresh always pre-empts any pending retry, and the Estate KPI tiles show a subtle "Reconnecting..." indicator while a retry is pending, distinguishing that state from genuinely empty data. -
Granting "All Connections" access no longer deletes a group's specific per-connection grants. Previously, adding an All Connections READ grant destructively removed existing per-connection grants, silently reducing a connection's READ/WRITE access to READ; specific and wildcard grants now coexist, and the effective access for each connection is the higher of the two. (#302)
-
Fix the Table and Index object-detail dashboards showing stale, pre-maintenance values in their overview tiles and Maintenance Info panel indefinitely, even after the collector recorded fresh, correct data. Running ANALYZE on a table corrected its live-tuple-count estimate downward, yet the dashboard kept showing the old, larger count and a stale or missing Last Analyze value. The latest-row mode of the metrics API (
GET /api/v1/metrics/querywithlimitandorder_by) ranked the table's entire history by the requestedorder_bycolumn and usedcollected_atonly to break exact ties, so it returned whichever historical sample had the highest column value rather than the most recent sample. Any column that can decrease over time, such as a tuple count after ANALYZE, VACUUM, or DELETE, or a dead-tuple count after VACUUM, could therefore return a permanently stale historical row. The query now reduces to each monitored entity's newest sample bycollected_atfirst, then ranks those already-latest rows by the requestedorder_bycolumn, so a request filtered to one table or index always returns that entity's true latest sample regardless of the sort column. -
Fix the Index object-detail dashboard's Scan Activity chart always showing "No index scan data available" for every index. The web client sent the index's own name as the
table_namequery parameter, but the metrics time-series query filtered onrelname, so no table ever matched an index name and the request returned no rows. The time-series query path also had no way to filter by index name, so a table with several indexes would have blended all of their scan activity together. The metrics API (GET /api/v1/metrics/query) now filters onindexrelname, the client's metrics-fetching hook accepts a newindexNameparameter, and the Scan Activity chart sends it instead of misusing the table-name parameter. -
Fix several dashboard charts rendering blank whenever their time series was perfectly flat, most visibly the Cache Hit Ratio Over Time chart for a new, essentially read-only sample database whose ratio held steady at 100% for its whole history. A flat series gave the value axis a zero-height range, with
minequal tomax, so the line or area had nothing to draw against and the chart appeared empty. The value axis now pads a small visible range around a flat value so the series renders, and it does so in a baseline-aware way for zero-anchored charts, such as bars and stacked or area fills, so their fill still renders from zero. (#336) -
Fix the Cluster Group settings dialog so it shows and preserves a group's description and its "Share with all users" setting, which previously appeared empty or unchecked and were lost when editing; the share flag is now persisted by the API. (#304)
-
Harden the collector against clean-exit restart loops that could stop the Dashboard from showing Top Queries. The collector now logs which signal triggered its shutdown, rather than exiting silently, and the default monitored probe and pool wait timeout (
monitored_max_wait_seconds) was raised from 60 to 120 seconds to give slow probes such aspg_stat_statementson large monitored databases more headroom before timing out. (#308) -
Harden the shipped Docker Compose collector service by running an init process as PID 1, adding a stop grace period for graceful shutdown, and raising the memory limit modestly from 256M to 512M; the collector connection-pool and timeout options are now documented in the sample configuration. (#308)
-
Fix the "Hide monitoring queries" toggle on the Server dashboard's Top Queries panel failing to hide the collector's and alerter's datastore query overhead. Only the collector's probe queries against monitored databases carried a marker, so its bulk
metrics.*inserts, partition maintenance, and change-detection reads, together with the alerter's metric-evaluation queries, stayed in the panel whenever the metadata datastore shared a PostgreSQL instance with the monitored databases, which is the usual deployment. The toggle now hides those statements as well. Each statement is tagged individually rather than excluding the metadata database wholesale, so queries that other tools run against that same database remain visible. The server's own datastore traffic for sessions, RBAC, conversations and the timeline, and some of the collector's and alerter's configuration reads, still appear in the panel.
On an existing installation the toggle only hides statements that
PostgreSQL first recorded after the upgrade. pg_stat_statements
identifies a statement by its parse tree, which ignores comments, so
an entry already present keeps the untagged text it was first seen
with and the filter never matches it; those entries run every
collection cycle, so they are never evicted either. Run SELECT
pg_stat_statements_reset(); once on each monitored instance after
upgrading for the toggle to take effect on statements already in the
view. (#364)
-
Fix the AI Overview and the server-info AI database analysis rendering blank with no error. The failure appeared when a local reasoning model served the request. Both paths capped their responses at a hardcoded 512 output tokens and ignored the configured
llm.max_tokensvalue. A reasoning model counts its thinking tokens against that budget. The model therefore spent the whole budget before emitting any answer text. Both paths now honourllm.max_tokens, which defaults to4096tokens and previously governed only the streaming chat proxy. A response that carries no text is now reported rather than cached as an empty result. TheGET /api/v1/server-info/{id}/ai-analysisendpoint returns502with a message that names the likely cause. (#399) -
Fix the collector testing the error from a failed configuration file load with
os.IsNotExist, which does not unwrap the wrapped error and so never matched, leaving two branches unreachable. Both checks now useerrors.Is, so a configuration file named with-configthat does not exist reports "specified config file not found" along with the path rather than a generic load failure, and an auto-discovered configuration file removed between discovery and load falls back to the compiled-in defaults as intended instead of aborting startup. (#421) -
Fix the Disk Space chart and the Disk Usage KPI tile on the server dashboard averaging
used_spaceandfree_spaceacross every mounted filesystem, including pseudo filesystems such astmpfsanddevtmpfs, so that the figures described no real filesystem. Both now describe a single real filesystem, chosen with a new Filesystem selector above the chart and defaulting to the filesystem closest to full, with the mount point shown in the tile label and the chart title; pseudo filesystems are excluded. The usage percentage is now derived from the filesystem's reported total size rather than from used plus free space, which differ on any filesystem with reserved blocks. TheGET /api/v1/metrics/queryendpoint gained amount_pointquery parameter, which filters both the time-series and the latest-row responses. The alerter's disk usage threshold metric took the highest usage across all mounts and so was pinned at 100% on any host with a squashfs mount, because a squashfs image is by construction fully used; the metric now ignores pseudo filesystems, matching the dashboard. (#428) -
Fix the collector running every past-due probe immediately at startup, with no stagger and no limit on how many ran at once. On a restart, every probe whose interval is shorter than the outage is past due, so concurrency scaled with the number of monitored connections multiplied by their probes; that burst exhausted a modest memory limit, and because the resulting restart put every probe back in the past-due state, the collector never recovered on its own. A probe that is past due or has never run now waits out a random delay, drawn from zero up to the smaller of its collection interval and the new
scheduler.startup_jitter_secondssetting (60 seconds by default, with 0 disabling the stagger), and its timer is realigned afterwards so the offset also spreads out steady-state collection. Every probe execution now holds a slot in a global semaphore bounded by the newscheduler.max_concurrent_probessetting (8 by default), so the memory and connection demand of active probe execution is capped by that setting rather than growing with the number of monitored connections multiplied by their probes; the scheduler still runs one goroutine per connection and probe pair, so that baseline continues to scale with the number of monitored connections. A probe holds its slot until the execution finishes or reachespool.monitored_max_wait_seconds, so the newscheduler.max_concurrent_probes_per_connectionsetting (2 by default, and clamped toscheduler.max_concurrent_probes) caps how many of those slots a single monitored connection may hold at once; without the ceiling, one server that accepts connections but answers slowly demands more slots than the cap provides and queues the probes of healthy connections behind its own. (#441) -
Fix anomaly detection ignoring the hourly and daily baselines the alerter had been writing. Baseline selection took the first row returned by a query ordered alphabetically by
period_type, which was always the globalallrow, so seasonality was never applied and the per-period warmup thresholds were unreachable. The engine now prefers the hourly baseline for the current hour, then the daily baseline for the current weekday, then the global baseline, and uses the first of those that passes its warmup gate; when none is warm, detection is suppressed for that value. Hour and weekday are derived in UTC both when baselines are written and when one is selected. The defaultbaselines.lookback_daysrises from 7 to 15: a baseline's span can never exceed the lookback, so at 7 days the daily tier's 336 hour warmup span could never be met and the daily step was silently skipped. The alerter now logs a warning on each baseline cycle for any warmup tier whosemin_span_hoursexceeds the lookback. Operators who setlookback_daysexplicitly should raise it to at least 15, or shortenwarmup.daily.min_span_hoursto match. (#408) -
Fix anomaly detection being permanently disabled for the 13 of 31 registry metrics that have no historical query. The alerter built a fallback baseline for those metrics from a single current sample, which could never pass the warmup gate, and rewrote it every baseline cycle. The fallback path is removed; such metrics are now skipped by baseline calculation and detection, and leftover baseline rows for them are deleted on each baseline cycle. (#408)
-
Fix anomaly detection mixing up databases on multi-database connections. Baseline lookup ignored the database name, the current value was taken from whichever database happened to be listed first, and the anomaly candidate was written without a database name, so alert deduplication collapsed every database on the connection. Baseline lookup, value selection and candidate creation are now all scoped to the database, so per-database metrics such as
cache_hit_ratio,deadlocks_deltaandtemp_files_deltaare scored against their own database's baseline and deduplicated per database. Each metric's latest values are also fetched once per rule rather than once per rule per connection. Active anomaly alerts for these three metrics raised before the upgrade carry no database name and so no longer match the per-database deduplication; the first detection after upgrading may add a second alert alongside such a row, which should be cleared by hand as anomaly alerts are not resolved automatically. (#408) -
Fix a probe staleness alert clearing itself at the moment collection on that probe stopped, which left an operator with no active alert for a server that had gone quiet. The alerter's cleanup pass read a staleness view that omits any probe which is unavailable, disabled, attached to a connection that is no longer monitored, or has never collected, and it treated absence from that view as proof that nothing was left to be stale about, so a probe that went unavailable (a monitored server losing an extension, or a probe run timing out) cleared the
metric_stalenessalert silently at debug log level. A probe that has gone unavailable now keeps any staleness alert that has already fired for it, and the alert's description is rewritten to say that collection has stopped and to give the recorded reason; the title is unchanged, so the alert stays recognisable in notification history. The rewritten description reverts to the evaluator's own wording once the probe collects again, and also on the way out when a held alert clears because an operator disabled the probe or because its connection is no longer monitored, so neither the clear notification nor the stored alert history announces the resolution in the words "Collection has stopped ...". On that clearing path the wording is rebuilt from the connection and probe configuration rows the staleness view joins against, so a held alert whose connection has gone altogether, or whose lookup fails, still clears carrying the held text. The line reporting the hold is logged once, when the decision or the recorded reason changes, rather than on each 30 second cleanup cycle. A probe that is absent because an operator disabled it, or because its connection is no longer monitored, still clears the alert, since both are deliberate actions, and both outcomes are now logged at the normal log level rather than at debug level. The staleness evaluator is unchanged and still skips unavailable probes, because an unavailable probe is a normal steady state for a server without the extension a probe needs, and raising a new alert for one would leave a permanent alert on every such probe; a probe that goes unavailable before its staleness alert has fired is reported by the newprobe_unavailablerule instead. (#465) -
Make
ai-dba-server -delete-userask for confirmation in every case. When the username was typed at the prompt rather than given with-username, the command read the confirmation through a second input buffer that never saw it, and deleted the account without asking; it did the same whenever the confirmation could not be read at all. The prompts now share one input buffer, and any answer other thanyoryes, including none, cancels the deletion. The-update-userprompts had the same split buffer and now share one too. (#588) -
Drop a connection's topology relationships from its old cluster when it moves to another one.
PUT /api/v1/connections/{id}/clusterandPOST /api/v1/clusters/{id}/serverschanged the connection's cluster but left the relationships naming it, so the old cluster kept showing edges to a server that was no longer a member. Both now delete those relationships, whichever end the connection is on, in the same transaction as the move, and keep any it already has in its new cluster. Saving a server's relationships now rechecks, under a row lock, that the server and every target are still members of the cluster, so a move made whilst the save is in progress can no longer leave a relationship behind; the save answers 400 instead. Topology auto-detection applies the same rule when it moves a connection to another cluster (#719). Relationships left behind by earlier moves are not removed retrospectively. (#707)
Removed
-
Drop support for PostgreSQL 14, which reached end of life on 12 November 2026; PostgreSQL 15 is now the oldest supported version, for both monitored servers and the datastore.
-
Drop support for building the web client with Node.js 20. The
enginesfield in the client'spackage.jsonnow requires Node.js 22.12 or later, where release 1.0.0 also accepted Node.js 20.19 or later on the 20.x line, and the Vitest 5 test runner that the client now uses requires Node.js 22.12 or later as well. Continuous integration tests the client on Node.js 22, 24 and 26. -
Remove the hard-coded allow-list of embedding model names, which rejected any model the OpenAI, Voyage AI, or Gemini provider had not been told about, whether the model was newly published by the provider or served under its own name by a local model server. The configured model name is now passed to the provider unchanged, so any model the provider accepts can be used, including models served by an OpenAI-protocol-compatible local server such as llama.cpp or vLLM. The per-provider defaults still apply when the configuration leaves the model empty, and an embedding wider than 4000 dimensions is still rejected with a dimension error when it is stored or used as a query vector. The alerter previously cut every embedding to 1536 dimensions before that check could run, silently truncating any wider model; it now keeps the model's native width, matching the server. Anomaly embeddings stored before this change by a model wider than 1536 dimensions remain at the truncated width, so their similarity to new embeddings is lower than it would otherwise be until they age out of the retention window. (#427)
-
Retire the
table_bloat_ratioalert rule, which duplicated thedead_tuple_ratiorule with a different denominator and fired on the same tables at a different threshold. The built-in rule is now disabled by default and its metric is no longer collected by the alerter; the rule definition remains so that historical alerts stay attributable, and any of its alerts that were active at upgrade time are cleared. Usedead_tuple_ratioto monitor table maintenance. (#409)
Security
-
GHSA-q38q-fvcv-jx25 (Critical): stop a database name override redirecting a monitored connection and exposing its stored credentials. The override accepted by
POST /api/v1/connections/{id}/query, by the MCP tools that take adatabase_nameargument and by a saved MCP session was inserted into the connection string without escaping, so any user with read access to one connection could have the server connect to another host with that connection's stored username and password, or override other driver settings. The connection string is now built with the name escaped, and a name that cannot name a real database, being empty, longer than 63 bytes or containing a control character, is refused. (#530; PR #538) -
Breaking: GHSA-68mj-c35x-m57h (High): enforce the scope of an API token owned by a superuser. The authorisation checks returned early on the owner's superuser status before consulting the token's scope, so a token scoped to one connection, MCP tool or admin permission carried full superuser access, and publicly available MCP tools ignored a token's tool scope whoever owned it. Each check now intersects the owner's rights with the token's scope. A
readconnection scope still lets a token acknowledge alerts and manage blackouts on that connection, whilst a blackout covering a cluster, a group or the estate needs the all-connections entry. A connection scope that mixes theAll Connectionsentry with particular connections, or names a connection twice or a negative ID, and an admin permission scope naming an unknown permission, are refused with400 Bad Request, and any change that would leave no enabled superuser is refused with409 Conflict. Existing integrations may break: a token that carries an explicit scope, most often one owned by a superuser, will receive403 Forbiddenwhere it previously succeeded, and no configuration option restores the old behaviour. Review every scoped token before upgrading; where one is refused afterwards, clear its scope, add the relevant wildcard, or narrow what the token is asked to do. (#471, #482; PR #528) -
GHSA-j7xf-vm5c-vwcf (High): stop read-only users running writes on monitored databases. The query endpoint and the MCP query tools judged a statement read-only from its first keyword, so a crafted
EXPLAIN ANALYZEof a data-modifying statement could pass the write-access check and run outside the read-only transaction, and aSELECTthat calls a side-effecting function such aspg_terminate_backend()orpg_reload_conf()ran as a read. AnEXPLAINis now classified by the statement it explains;SELECT INTO, a row-lockingSELECT, aWITHquery containingMERGE, and aSELECTthat calls a documented PostgreSQL or common extension function that signals or changes server state now count as writes, needing write access and the confirmation prompt. Functions called through a user-defined wrapper or view are not detected. The MCP tools apply the same check:query_databaserefuses such aSELECT,execute_explainrefuses to runEXPLAIN ANALYZEon it, andtest_queryandcount_rowsrefuse it. The statements of a confirmed write now run on a single connection, so aBEGINandCOMMITin the same request take effect together, and a request that leaves a transaction open is rolled back and reports an error. (#530, #646; PRs #538, #659) -
GHSA-fc39-23x3-g3v4 (High): stop the
manage_userspermission granting superuser status. The create and update user endpoints acceptedis_superuserfrom any caller holdingmanage_users, which a group can be granted, so such a user could make themselves or another account a superuser, reset a superuser's password and sign in as them, or delete superuser accounts.POST /api/v1/rbac/usersandPUT /api/v1/rbac/users/{id}now refuse a request carryingis_superuserwith403 Forbiddenunless the caller is already a superuser, and editing or deleting an account that is a superuser now needs a superuser too, checked inside the change's own transaction. (#497, #588; PRs #553, #604) -
GHSA-xj7f-8wf7-q543 (High): stop the
manage_token_scopespermission working as a route to superuser. A holder could create a token for any user, superusers included, and widen or clear the scope of any token a superuser did not own, and so act as that account. A caller who is not a superuser can now create a token only for themselves, and widen or clear the scope only of their own tokens; any holder can still narrow or delete any token, but a scope change to someone else's token must narrow it in every scope type. The refusal is403 Forbidden, and the owner's superuser status is checked inside the change's own transaction. A scope change naming an unknown MCP privilege is refused with400 Bad Request, a token caller's scope change is refused with409 Conflictif the target token's scope changes in the meantime, and a scope update that sets several scope kinds is written in one transaction. In the web client, a user who is not a superuser sees the token owner fixed to themselves, is offered only narrowing choices when editing someone else's token, and no longer sees edit or delete actions on a superuser's token. (#522, #607; PRs #661, #610) -
GHSA-w678-xh7h-7gvc (High): stop a connection's
hostaddrbypassing the host and port checks, which allowed server-side request forgery (SSRF) and disclosure of the connection's stored credentials to an arbitrary target. The connection string prefershostaddrtohost, but onlyhostpassed theconnection_securitychecks, so a permitted host name could front any internal address, and a craftedhostaddrcould add a second target and a port that no check examined. Release 1.0.0 is affected. Ahostaddrmust now be a literal IP address and passes the same blocklist, allowlist and internal network checks ashost, and ahostcontaining,,/,?,@,#,%or whitespace is refused, as is ahostwith a[or]that does not enclose a single IPv6 literal. The server and the collector also refuse a stored connection with such ahostor ahostaddrthat is not an IP address before connecting, and connection string errors returned by the API and the MCP tools are now generic, so they no longer echo SSL file paths or passwords. -
GHSA-82cj-f67f-fc5x (High): stop a quote in a database name, user name or password injecting settings into the collector's connection strings. The collector escaped a single quote in a connection string value by doubling it, but libpq and pgx expect a backslash, so the quote ended the value early and the rest was read as further settings. A user who can only create a database on a monitored server could name it so that the collector connected to a host of their choosing and sent it the monitoring role's credentials, or read SSL files outside
connection_security.ssl_cert_dir. Release 1.0.0 is affected. Connection string values are now escaped as libpq specifies. -
Breaking: GHSA-2h8m-pf3h-xwpf (Medium): limit the
query_datastoreMCP tool to the connections the caller can read. The tool ran read-only SQL over the whole datastore, so any caller holding the tool privilege, including an API token scoped to one connection, could read the collected metrics, configuration and connection records of every monitored server. The tool is now structured: it rejects asqlorqueryargument, and a client must name asourceand describe the query withcolumns,connection_ids, a time range,filters, ashape(rows,latestoraggregate),group_by,aggregation,buckets,order_byand paging instead. Connection-specific rows cover only the connections the caller can see, further limited to an API token's connection scope; estate-wide configuration rows, such as alert rules, alerter settings, blackouts and probe settings, remain readable through an explicit list of columns, and stored connection passwords and certificates are never available. Settings, HBA options, statement text, connection errors and alert text that can hold a credential are masked on a best-effort basis. Metric sources default to the last hour for every shape,page_sizedefaults to 100 and is capped at 1000, andexplainreturns the plan without executing the query, for callers who can access every connection. A group grant or token scope that includesquery_datastorealso gives access to the newdescribe_datastoretool, and counter rates and deltas remain the job ofquery_metrics. (#566; PR #638) -
GHSA-rxm6-9223-c78c (Medium): mask credentials in collected settings, HBA rules and statement text. The collector stored, and the server returned unmasked to any user with read access to the connection, values that can carry secrets: the replication password in
primary_conninfo, secrets embedded inarchive_command,archive_cleanup_command,restore_commandorrecovery_end_command, theldapbindpasswdandradiussecretsoptions inpg_hba.conf, and passwords in captured statement text such asALTER ROLEwith aPASSWORDclause. Thepg_settings,pg_hba_file_rules,pg_stat_activity,pg_stat_statementsandspock_exception_logprobes now store these values as********, keeping a shell command's program name, and collector schema migrations 24 to 26 mask the rows already stored; migration 26 reads the whole of each statement table, so it can take a while on a large datastore. The server masks the same values again inGET /api/v1/metrics/latest, the latest-row mode ofGET /api/v1/metrics/query,GET /api/v1/metrics/top-queriesand the configuration change events fromGET /api/v1/timeline/events. The alerter masks passwords in the descriptions of connection error alerts, collector schema migration 27 masks those already stored, and the server masks alert descriptions and AI analyses inGET /api/v1/alertson a best-effort basis. A change to a password or to a command's arguments alone is no longer recorded as a configuration change. Masking cannot recall a value that has already been read, and old values can survive in dead tuples, the WAL, earlier backups of the datastore and notifications already sent, so rotate any password or secret that these settings or statements held whilst an earlier version was collecting. (#643, #670, #676, #679, #680; PRs #658, #671, #685, #693) -
GHSA-c57h-cq9p-fpqq (Medium): stop the
query_databaseMCP tool connecting to hosts named in the query text. Called without aconnection_id, the tool connected to apostgres://connection string found in the query, without checking that it was a registered connection the caller could access. The tool now runs only on the givenconnection_id, after the usual access check, or on the caller's selected connection, and any connection string in the query text reaches PostgreSQL as part of the SQL; to query another database on a stored connection, passconnection_idwithdatabase_name. The tool is also now read-only for every caller: writes, DDL, aSELECTthat calls a side-effecting function and a query holding more than one statement are refused, even for a superuser. It stops a statement once it has readlimitrows, a trailing--comment no longer hides theLIMITandOFFSETit adds, so asking for the next page now advances, and a query ending in a semicolon no longer fails with a syntax error. (#662, #663; PR #692) -
GHSA-cwr7-xx93-fqjv (Medium): stop a holder of the
manage_connectionspermission updating, deleting or sharing a private connection they cannot read.PUTandDELETEon/api/v1/connections/{id}skipped the connection access check for such a caller, who could edit or delete another user's private connection, mark it shared, or point it at a different host whilst keeping the stored password. Both now answer404 Not Foundfor a connection the caller cannot see, exactly as for a missing one; a superuser is unaffected. (#710; PR #720) -
GHSA-34h3-vcf9-7f7f (Medium): stop a monitored connection or a notification channel reaching an internal address that the
connection_securitychecks should have refused. The internal ranges left out the IPv6 unspecified address::, which reaches the local host, and the IPv6 ranges that embed an IPv4 address, so a connection host,hostaddror webhook URL could name them. The connection checks also ran only when a connection was saved, so a host name could pass and later resolve to an internal address (DNS rebinding). The alerter likewise checked a webhook channel's host before delivery and then resolved the host again to connect, and checked Slack, Mattermost and email (SMTP) channels not at all. The server's "send test notification" path checked a webhook, Slack, Mattermost or email channel's host and then resolved the host again to connect. Release 1.0.0 is affected. The server now refuses unspecified, link-local and cloud metadata addresses for a monitored connection whatever itsconnection_securitysettings, applies those settings to every other address the connection resolves to each time it connects, and connects only to the addresses that pass; the collector refuses unspecified, link-local and cloud metadata addresses each time it connects; the alerter resolves the host of every notification delivery, on every channel, and connects only to an address that its delivery policy permits; the server's test send, on every channel, refuses unspecified, link-local and cloud metadata addresses and then applies the server'sconnection_securityrules to every address that the send connects to, answering a refusal with a generic 400 error (a 502 for Telegram); and the internal ranges now include::,::/96,::ffff:0:0:0/96,64:ff9b::/96,64:ff9b:1::/48,2002::/16andfec0::/10. The always-refused cloud metadata addresses arefd00:ec2::254,fd00:ec2::23,fd20:ce::254and100.100.100.200, and an address in64:ff9b:1::/48is also refused when the IPv4 address at any RFC 6052 offset within the prefix is in169.254.0.0/16or is100.100.100.200. A test send uses the server'sconnection_securitysettings and a delivery uses the alerter'snotifications.allow_internal_networksoption, so one can succeed where the other is refused; see Test Sends. -
GHSA-cj7p-8h5w-c4xr (Medium): stop a user who can create a connection making the server open any file it can read. The server reads a connection's SSL certificate, key and root certificate files when it parses the connection string, even under
sslmode=disable, and the error revealed whether a file existed. These paths must now be absolute and resolve, following symbolic links, to a location inside the directory named by the newconnection_security.ssl_cert_diroption; the server checks them when a connection is created or updated and again before connecting. The collector, which also reads these files when it connects, checks each stored connection against its ownconnection_security.ssl_cert_diroption before connecting, and does not monitor a connection that fails the check. A root certificate value ofsystemis always allowed. Release 1.0.0 is affected. -
GHSA-7jmj-836r-3w4r (Medium): stop the server writing database passwords to its log and returning them in errors. The server masked only the password in the user information of a URI connection string, so a password in a keyword/value connection string, such as one set through
PGEDGE_POSTGRES_CONNECTION_STRING, or in apasswordorsslpasswordparameter appeared in the[DATABASE]log whenPGEDGE_DB_LOG_LEVELwasinfoor higher, and in MCP tool output that names the default connection. A metadata load for a connection that the server did not hold also returned the unmasked connection string in its error. Release 1.0.0 is affected. The server now maskspasswordandsslpasswordparameters in both connection string forms, and the metadata load and query connection errors no longer quote the connection string. Rotate any password that may have reached a log. -
Update
github.com/jackc/pgx/v5from 5.9.2, as used in release 1.0.0, to 5.12.0 (High), fixing two driver vulnerabilities. GHSA-j38q-h3ph-jjpq allowed SQL injection in simple-protocol batches when a queued query ended in a--comment, and GHSA-95fj-3fxw-g62g skipped therequire_authandchannel_bindingchecks when a server omittedAuthenticationOk, so a rogue or man-in-the-middle PostgreSQL server could be accepted. Neither is reachable from the code in release 1.0.0, which sends no query batches and never setsrequire_authorchannel_binding; the second could affect only a connection string that an operator wrote with either option, such as one inPGEDGE_POSTGRES_CONNECTION_STRING. -
Update
golang.org/x/textfrom 0.33.0, as used in release 1.0.0, to 0.44.0, fixing an infinite loop on invalid input (CVE-2026-56852) and a panic when parsing crafted input in thesecure/precispackage (CVE-2026-56851). -
Build with, and require, Go 1.26.11 in place of the Go 1.26.2 used for release 1.0.0, fixing the following standard-library vulnerabilities reachable from the code in that release:
- in
net/http, covering HTTP/1 and HTTP/2, CVE-2026-97054 (HTTP/1 server crash from aMaxBytesReaderheader race), CVE-2026-97032 (HTTP/2 server crash from an HPACK encoder race), CVE-2026-78659 (HTTP/2 server memory exhaustion throughTrailerheaders), CVE-2026-78669 (excessive CPU from repeated window changes), CVE-2026-78663 (double flow-control refund on HTTP/2 server streams), CVE-2026-78660 (HTTP/2 transport accepting malformed framing headers), CVE-2026-33814 (HTTP/2 transport infinite loop on a badSETTINGS_MAX_FRAME_SIZE), CVE-2026-94439 (HTTP/1 server desynchronisation after a 2xxCONNECTresponse), CVE-2026-56866 (HTTP/1 client desynchronisation after aCONNECTrejection) and CVE-2026-56853 (ReadHeaderTimeoutnot applied to the unencrypted HTTP/2 check). - in
crypto/tlsandcrypto/x509, CVE-2026-111943 (distrust of compromised intermediate certificates), CVE-2026-56862 (unbounded post-handshake messages), CVE-2026-97031 (malformed Encrypted Client Hello outer extension references), CVE-2026-42505 (Encrypted Client Hello privacy leak) and CVE-2026-27145 (inefficient hostname parsing incrypto/x509). - in
html/template, CVE-2026-39826 and CVE-2026-39823 (escaper bypasses leading to cross-site scripting) and CVE-2026-56858, CVE-2026-94448 and CVE-2026-97030 (JavaScript context tracking errors). - in parsing and encoding, CVE-2026-33818 (
encoding/asn1recursion depth), CVE-2026-56859 (encoding/xmlrecursion depth), CVE-2026-94440 (MIME header memory limit bypass innet/textprotoandmime/multipart), CVE-2026-42507 (unescaped input innet/textprotoerrors), CVE-2026-42499 and CVE-2026-39820 (quadratic parsing innet/mail), CVE-2026-56860 (quadratic complexity innet/url) and CVE-2026-39821 (Punycode label validation inidna). - in
net, CVE-2026-39836 (panic inDialandLookupPorton a NUL byte, on Windows only).
- in
-
Update
golang.org/x/cryptoto v0.58.0 in every Go module, fixing the advisories against itsssh,ssh/agentandssh/knownhostspackages, along withgolang.org/x/textv0.44.0 and, in the server,github.com/go-jose/go-jose/v4v4.1.9. -
Drop trust in eight compromised eMudhra intermediate CAs (CVE-2026-111943). Go 1.26.10 and later, and Go 1.27.3 and later on the 1.27 line, add them to the
crypto/x509denylist, and the collector, server and alerter now build with Go 1.26.11; Go 1.27.0 to 1.27.2 remain vulnerable. The documentation build'scertifipin moves to 2026.10.23, which removes them. -
Refuse a probe, alert or notification channel override written at cluster or group scope unless the caller can see every connection in that cluster or group (#716). Seeing one member was previously enough to change the overrides that apply to all of them, including connections the caller cannot see. Such a write, through the override endpoints or through
PUT /api/v1/probe-configs/{id}on a cluster or group configuration, now answers 403 with the same message as a caller without the permission, so the refusal does not reveal which members are hidden or how many. The same rule now applies to deleting a cluster or cluster group and to moving a cluster to another group, including the auto-detectedserver-{id}andcluster-spock-{prefix}clusters, since each changes the overrides reaching every member. Removing a connection the caller cannot see from a cluster, throughDELETE /api/v1/clusters/{id}/servers/{connectionId}, now answers 404 as for a connection that is not a member, rather than detaching it and revealing that it belonged to the cluster. Superusers and users with a grant on all connections are unaffected, and reading cluster and group overrides is unchanged. -
Refuse a blackout or blackout schedule created, updated, stopped or deleted at cluster or group scope unless the caller can see every connection in that cluster or group (#732). A user with
manage_blackoutscould previously silence alerts on connections they cannot see, by naming a cluster or group that holds them. Such a write now answers 403 with the same message as a caller without the permission, as for overrides (#716). A blackout or schedule whose connection, cluster or group the caller cannot see at all now answers 404 as though it did not exist; for an API token, that includes creating one on a server outside the token's connection scope, or moving a schedule onto one, which previously answered 403. Creating, updating, stopping or deleting an estate-wide blackout or schedule, or moving a schedule onto or off the estate, now needs the caller to see every connection, and answers the same 403 otherwise. Creating a blackout or schedule, or moving a schedule, onto a connection, cluster or group that does not exist now answers 404 rather than 500. Superusers and users with a grant on all connections are unaffected, and reading blackouts is unchanged. -
Refuse a change to an alert rule, a global probe configuration, or a notification channel or its email recipients unless the caller can see every connection (#744). Each of these applies to the whole estate, so a user with
manage_alert_rules,manage_probesormanage_notification_channelsand a grant on only some connections could previously change thresholds, collection or alert delivery for connections they cannot see. Such a write, including creating or deleting a channel or recipient, now answers 403 with the same message as a caller without the permission, as for estate-wide blackouts (#732). Superusers and users with a grant on all connections are unaffected, reading these objects is unchanged, and a probe configuration on a single connection, cluster or group still follows the rule for overrides (#716). -
Require a connection on every server-scope blackout and blackout schedule in the datastore (#732). The collector's schema migration 30 converts any server-scope row stored without a connection to an estate row, which is how the alerter already treated it, and clears any group or cluster stored on a server-scope row, which the alerter ignored. A row without a connection that names a database matched only that database, so before it becomes an estate row the migration deletes such a blackout if it has not started, ends it now if it is active, and disables such a schedule.
-
Reset the session on each pooled connection to a monitored database when a tool call releases it (#709). A tool call could previously leave settings such as
search_path, temporary tables, prepared statements or advisory locks on a shared connection, where the next caller to borrow it would see them. The server now runsDISCARD ALLas each connection returns to the pool, and closes the connection instead if the reset fails. -
Answer a write to a hidden override scope or alert exactly as a write to a missing one (#672). The probe, alert and channel override
PUTandDELETEroutes checked the admin permission, the token scope and the request body before checking that the caller could see the server, cluster or group named, so a hidden target answered 403 or 400, or was changed, where a missing one answered 404. They now answer 404 for any target the caller cannot see, and an API token writing to a server outside its connection scope gets 404 rather than 403. Acknowledging, clearing the acknowledgement of, or saving the analysis of an alert on a connection the caller cannot see, or a system alert they may not see, likewise answers 404 rather than 403. -
Check connection visibility on the cluster relationship routes (#678).
GET /api/v1/clusters/{id}/relationshipsnow returns 404 for a cluster with no member the caller can see, and lists only the relationships whose source and target connections are both visible; previously it returned every relationship in the cluster to any authenticated caller. The relationship writes still require themanage_connectionspermission, and a caller without unrestricted visibility must now also see the source connection, every requested target, and the target of any manual relationship that a rewrite or clear would delete. That last check is made by the delete itself, so a relationship to a hidden connection written whilst the request is in flight is not lost. Deleting a relationship by id needs both of its connections to be visible and the relationship to belong to the addressed cluster. Saving areplicates_withrelationship no longer discards the target connection's other manual relationships when it adds the reverse row.GET /api/v1/connections/{id}/clusterlikewise now omits relationships to connections the caller cannot see, and lists only the clusters with a visible member, asGET /api/v1/clusters/listalready did. -
Check what a cluster write points at (#673, #708). A caller holding
manage_connectionscould create a cluster in, or move one into, a cluster group they cannot see, and could rename, move or dismiss an auto-detected cluster they cannot see by itsserver-{id}orcluster-spock-{prefix}ID.POST /api/v1/clusters,POST /api/v1/cluster-groups/{id}/clustersandPUT /api/v1/clusters/{id}now answer a hidden group with the same 404 as a missing one, where a missing group previously gave a 500; a group with no member connections stays writable by its owner, and the default group stays writable to every caller holdingmanage_connections, since a cluster created without a group lands there.PUTandDELETEon an auto-detected cluster ID answer 404 unless the caller can see the cluster, and a suppliedauto_cluster_keymust name the same cluster. Superusers, and callers who can see every connection, are unaffected. -
Apply one visibility rule to cluster groups with no member connections (#705). Such a group is now visible to its owner and to callers who can see every connection, and answered with the same 404 as a missing group for everyone else, on
GET /api/v1/cluster-groups,GET /api/v1/cluster-groups/{id},GET /api/v1/cluster-groups/{id}/clusters, andPUTandDELETEon/api/v1/cluster-groups/{id}. Previously any caller holdingmanage_connectionscould rename another user's empty group that theGEThid from them, whilst the group's own owner could neither see nor delete it. An API token whose connection scope does not cover every connection is not treated as its owner's.GET /api/v1/cluster-groups/{id}/clustersnow also answers 404 for a group that does not exist, where callers who can see every connection previously got an empty list. -
Quote untrusted text in the alerter's tier 3 classification and re-evaluation prompts. Database, metric and connection names, alert titles, node roles, acknowledgement notes and user names, and the stored tier 3 result were written into the prompts verbatim, so anyone able to create a database or acknowledge an alert could add a line such as
## Instructionsand steer whether an anomaly is suppressed or an acknowledged alert is cleared. Each such value is now written as a double-quoted string with line breaks and other non-printable characters escaped, both prompts tell the model that quoted values are data rather than instructions, and the tier 3 prompt lists each past acknowledgement as named fields, as re-evaluation already did. The re-evaluation fingerprint covers the prompt text, so after upgrading, every acknowledged alert with a stored "keep" verdict is sent back to the reasoning model once, the next time it falls due. (#682) -
Check connection visibility on the probe configuration read endpoints.
GET /api/v1/probe-configs?connection_id=NandGET /api/v1/probe-configs/{id}returned a connection's probe settings to any signed-in user, including one with no grants. The list now answers 404 for a connection the caller cannot see, and the single-item endpoint answers 404 for a configuration whose server, cluster or group the caller cannot see, in both cases exactly as for one that does not exist; an API token sees only the connections in its scope. Global defaults stay visible to every caller.PUT /api/v1/probe-configs/{id}applies the same check before reading the request body, so a user withmanage_probescan no longer read or confirm a configuration they cannot see. The unfiltered list now returns only the global defaults, as documented; it previously also returned the cluster and group overrides, whatever cluster or group they belonged to. The alerter made the same mistake when computing probe staleness: a cluster or group override duplicated each staleness row, and could supply the interval for a stalled probe's alert in place of the global default. (#647) -
Stop the cluster-group update and delete endpoints confirming that a group the caller cannot see exists.
PUTandDELETEon/api/v1/cluster-groups/{id}checked ownership before visibility, so a caller who neither owned nor could see a group got 403 where a missing id got 404. Both now check visibility first and answer an unseen group with the same 404 as a missing one; a visible group the caller does not own still gets 403, and the owner of a group with no member connections can still rename it. An API token whose connection scope does not cover every connection is now refused before the lookup, so its 403 is the same whether or not the group exists. The get, update and delete endpoints for/api/v1/cluster-groups/{id}, andGET /api/v1/cluster-groups/{id}/clusters, now also run the same database queries for a missing group as for one the caller cannot see.PUT /api/v1/conversations/{id}likewise answered another user's conversation with 403, and now answers 404 as the get, rename and delete endpoints already did. (#649) -
Mask passwords in the connection error that the
list_connectionsMCP tool and the cluster topology fromGET /api/v1/clustersreturn (#683). Both returned the collector's last connection error for each server as it was stored, so a libpq error quoting a connection string or URI with its password reached the web client and, throughlist_connections, the chat model. The error is now cut and masked in the same way as inquery_datastore, on a best-effort basis. -
Delete a user with
ai-dba-server -delete-useronly when the confirmation is an explicityoryes. The answer was checked only when a whole line could be read, so input that ended without a newline skipped the check and deleted the user:printf n |deleted on an explicit "n", as did running the command with stdin redirected from/dev/null. The answer is now read even when the input ends without a newline, soprintf y |still confirms whilst anything else declines, and a read error other than end of input stops the command with an error rather than deleting. (#608) -
Escape alert text for the chat service's markup in Slack and Mattermost notifications. The alerter previously escaped template values only for JSON, so an alert title, description, server name or database name containing
<!channel>or<https://example.com|click>became a channel-wide mention or a disguised link in Slack, and one containing@channel,[text](url)or emphasis markers took effect in Mattermost. Slack values now have&,<and>replaced with HTML entities; Mattermost values have each Markdown character escaped with a backslash and a zero-width space inserted after each@. Only the values are escaped, so the links, mentions and formatting an administrator writes into a custom template still work. The fire, clear and reminder templates are all covered. (#585) -
Stop the alert history, blackout, metric baseline and timeline tools revealing connections the caller cannot see. Asked for an unknown connection ID, the first three listed the IDs and names of up to 20 connections regardless of the caller's access, and all four answered a forbidden ID differently from a missing one, so a caller could probe which IDs exist. The tools now check access before looking the connection up, give the same "connection not found or not accessible" answer for a missing ID, a forbidden one and a failed access check, and suggest only connections the caller can see. (#571)
-
Treat a missing RBAC checker as denying access rather than granting it. The alert history, blackout, metric baseline, alert rule, metric query, timeline and connection list tools, the timeline and overview endpoints, and the shared connection visibility helper each skipped their access check when handed no checker, and so returned every connection's data; they now return nothing, or a not-found or permission error, and the admin handlers for alert rules, overrides, blackouts, notification channels and probes answer 403 instead of panicking. The production server always builds a checker, so this closes a latent hole rather than a live one. The overview stream also stops subscribing a signed-in user who can see no connections to the estate-wide feed, which had sent them the overview of every connection, and the connection list tool now answers with an error when the caller's visible connections cannot be resolved, where it used to return every connection unfiltered. (#561)
-
Stop the Slack, Mattermost and generic webhook channels putting their endpoint URL into the alerter log, into
notification_history.error_messageand into the server log when a delivery or a test send fails. For Slack and Mattermost the whole webhook URL is the credential, and a generic webhook endpoint may carry one in its path or query string, yet a routine transport failure produced an error that quoted the URL in full. Such errors now name only the operation, the host and the cause. A failing endpoint's response body has its control characters removed, has the channel's own URL path, query string and userinfo removed wherever they appear (with or without a scheme in front), and is capped at 256 bytes before it is echoed; the Telegram channel's echoes lose their control characters in the same way. The server now also rejects a webhookhttp_methodother thanGET,POST,PUTorPATCHwhen a channel is created or updated. (#498) -
Bound the rows held in memory when a statement runs over the simple query protocol, which is the path taken by an
EXPLAINcontaining a$1placeholder. That path could not add aLIMITclause, because it sends the statement text unaltered, and it kept every row the server returned, so a large result could exhaust the server's memory. Rows past the same 500-row limit the ordinary path applies are now read but not kept, and the response reports the result as truncated in the way it already does elsewhere. The rest of the result is still read, so the connection remains usable for the following statements in the request. (#530) -
Fix a configuration file that omits
http.auth.max_failed_attempts_before_lockoutsilently disabling account lockout. The setting was a plain integer, so an omitted key arrived at the merge step as0, which passed the>= 0test and overwrote the default of10; every installation whose configuration did not name the key was therefore running with lockout turned off, leavingPOST /api/v1/auth/login, which is unauthenticated by design, defended against password guessing only by rate limiting keyed on the client address: the failed-attempt allowance is cleared by any successful login, a cap of 20 login requests a minute for the address remains, and neither count is shared between server instances. The setting is now held as a pointer, in the same way ashttp.auth.audit_retention_days, so that an omitted key keeps the default of10whilst an explicit0still disables lockout deliberately. Restart the server to apply the fix: the threshold is read once at start-up, so aSIGHUPreload leaves the running server with the value it started with, and a reload that changes the setting now says so. A server that starts with lockout switched off warns at every start. (#473) -
Stop following HTTP redirects when the alerter delivers a notification. This changes the behaviour of every existing Slack, Mattermost, Telegram and generic webhook channel: a provider that answers a delivery with a 3xx response now fails that delivery, where the redirect was previously followed without comment. Such a delivery is retried on the usual backoff schedule and then recorded as
failed, with the 3xx status code named in the stored error. The credential these channels use sits in the request URL, because the Slack and Mattermost webhook URLs are secrets in themselves and the Telegram URL carries the bot token in its path. Go copies the previous request's full URL into theRefererheader of a redirected request, so following a redirect handed the credential to the host named in theLocationheader. A generic webhook channel gains a second protection: the alerter checked the endpoint host before sending, and a redirect could previously carry the request past that check to a private or metadata host; the alerter now also checks every address that it connects to. The change is preventive: reaching the behaviour required a hostile or compromised endpoint answering a delivery with a redirect, and no credential is known to have been disclosed. The server's test-send path has always refused redirects, so delivery now matches it. Where an endpoint relies on a redirect, configure its final URL in the channel; email channels are unaffected, because they deliver over SMTP. (#475) -
Set a server-side
statement_timeouton the server's datastore connection pool, defaulting to 30 seconds and configurable with the newdatabase.statement_timeoutoption. The pool previously relied entirely on the handlers cancelling their own queries, which is a request sent over the connection: if it is lost, or the backend is not at an interruptible point, the query keeps running and keeps holding one of the pool's connections, so a handful of those could starve the whole API. The default matches the longest deadline any handler grants a datastore query, so nothing that completed before now fails; the collector owns the schema migrations, including the partitioned-index build that can run for much longer than this, and they run on the collector's own pool, which is unaffected. (#387) -
Ignore a blank password when updating a database connection, so an empty or whitespace-only password can no longer overwrite the stored credential. The datastore now enforces the "leave blank to keep unchanged" rule server-side as defence-in-depth, rather than relying on the web client alone. The create path, which still requires a non-empty password, is unaffected. (#332)
-
Refuse a metrics request naming more than 100
connection_idswith400 Bad Request; the web client batches its requests to stay within the limit. (#520) -
Update the web client's ECharts dependency to v6.1.0, which resolves a cross-site scripting advisory, and move the
vitestdevelopment dependencies to v5.1.0, which refreshes the transitivepostcss,js-yaml,nanoid,brace-expansion,fflate, andvitepackages. Before these changesnpm auditreported eleven findings (six moderate, five high), of which only the ECharts advisory affected the shipped client bundle; the rest sat in the development dependency tree.
[1.0.0] - 2026-06-08
This release is the first general-availability release of the pgEdge AI DBA Workbench, graduating the project from beta to a stable, supported 1.0 line.
Added
- Add a
password_fileoption to the server'sdatabase:YAML block, allowing the server to read the datastore password from a file. The server uses this file only when no inlinepasswordvalue and no CLI password flag are set, bringing the server in line with the collector and alerter. (#267)
Changed
-
Unify how the server, collector, and alerter read secrets from files onto a single hardened helper. The change covers database and user passwords, service tokens, LLM and embedding API keys, server and notification secrets, proxy header values, and binary encryption keys. Several behaviours below are backward-incompatible, so operators upgrading the collector or alerter should review them before rolling out the new binaries. (#267)
-
Trim only the trailing newline from a secret file, rather than stripping all surrounding whitespace. This applies to passwords, tokens, API keys, server and notification secrets, and proxy header values. Secrets that legitimately contain leading, trailing, or interior spaces are now preserved verbatim. Operational: operators who relied on the old whitespace-stripping behaviour may need to re-create affected secret files so the stored value matches the intended secret. (#267)
-
Treat a configured-but-empty secret or password file as a hard startup error, instead of silently treating the file as "no value". Previously an empty file fell through to
.pgpassor a passwordless connection, which could mask a misconfigured deployment. Operators must ensure every configured secret file contains a value before starting the new binaries. (#267) -
Accept any owner-only permission mode, such as
0400or0600, for the binary key loaders that read the server encryption key and the alerter notification secret key, instead of requiring exactly0600. Group- or world-accessible modes such as0640and0644are still rejected. (#267) -
Expand a leading
~in a secret or server-secret file path to the user's home directory consistently across all components. (#267) -
Allow the server's
-db-password-fileflag to accept paths containing.., so legitimate relative paths now work. The flag previously rejected any path containing... (#267) -
Raise the web client's minimum Node.js version to 20.19 and enforce it through an
enginesfield in the clientpackage.json(^20.19.0 || >=22.12.0), with a matching.nvmrc. The client's build tooling, including Vite 8 and the@csstoolspackages, requires Node.js 20.19 or later, so older releases such as Node.js 18 failed the build with a cryptic error. The documented prerequisite now states the supported range (Node.js 20.19 or later on the 20.x line, or 22.12 or later) everywhere it appears. (#272)
Fixed
-
Fix the AI assistant "Ellie" and the client-side analysis prompts generating SQL against the old combined
pg_stat_bgwritercolumns such ascheckpoints_timed,checkpoints_req, andbuffers_checkpoint, which fail on PostgreSQL 17 and later. Those checkpoint statistics moved into the newpg_stat_checkpointerview in PostgreSQL 17. The system prompts now instruct the model to querypg_stat_checkpointeron PostgreSQL 17 and later and the combinedpg_stat_bgwriterview on PostgreSQL 16 and earlier, choosing the view based on the target server's version. (#286) -
Fix the
get_metric_baselinesMCP tool returning no baselines when themetric_namefilter used an unqualified shorthand such ascache_hit_ratio, because the tool matched the stored fully-qualified name such aspg_stat_database.cache_hit_ratioonly on an exact match. The filter now applies a parameterised, case-insensitive containment match, with literal%and_matched literally, and a filter that matches nothing returns the available fully-qualified metric names within the caller's RBAC scope so the model can retry. (#287) -
Fix the alerter failing to load its server secret when
secret_filewas unset, with no fallback search. The alerter now resolves a default secret file the way the collector and server already do; whensecret_fileis not set, it searches the per-user config directory first and then/etc/pgedge. This brings the three services to parity and makes the search order documented inexamples/ai-dba-alerter.yamlaccurate. (#291) -
Fix the alerter's Gemini provider lacking a configurable embedding model. The
llm.geminisection now exposes anembedding_modeloption that defaults togemini-embedding-001; the alerter previously offered no embedding model field for Gemini. When Gemini is the embedding provider, the configured model must match the model that the KB Builder used to produce the knowledgebase. (#284) -
Fix the documented Gemini embedding model list, which referenced the deprecated
text-embedding-004andembedding-001models that Google has retired. The supported models are nowgemini-embedding-001(the default),gemini-embedding-2, andgemini-embedding-2-preview, all of which output 3072 dimensions rather than the previously documented 768. The server's knowledgebase status log now reports the Gemini API key as loaded. (#285) -
Fix the Admin Panel's Create Group, Edit Group, and Create Token dialogs accepting unsupported special characters and over-long Name values. RBAC group and token names now reuse the app's shared name validator on both the client and the server: after trimming, a name must be non-empty, at most 255 characters, and may contain only letters, numbers, spaces, and the characters
. _ - ( ). Creating a group with a name that already exists now returns a clear "A group with this name already exists" conflict (HTTP 409) instead of a generic server error. (#273) -
Fix the Admin Panel's Create User and Edit User dialogs accepting invalid usernames and email addresses and surfacing unhelpful errors. A username with special characters, an invalid email such as
notanemailortest@, or a duplicate username previously returned a generic server error or stored silently. The client and server now apply matching rules: the server validates the username, validates the email format, and returns HTTP 400 with the specific rule, or HTTP 409 "Username already taken" for a duplicate. The dialogs show inline, field-level errors and block the save until the input is valid. (#271) -
Resolve two moderate-severity npm vulnerabilities reported in the web client's dependency tree by updating the affected transitive dependencies. (#272)
-
Fix the server silently swallowing read errors for configured LLM, embedding, and knowledgebase API-key files. The server previously proceeded with no key when such a file was unreadable or empty; an unreadable or empty configured key file now fails loudly at startup. Leaving a key-file path unset remains valid and is not an error. (#267)
-
Fix name fields for server groups, clusters, and servers accepting special characters such as
<>!@#$%. Validation previously rejected only empty input, so disallowed characters were stored unchanged. The server and client now apply the same rule: after trimming, a name must be non-empty, at most 255 characters, and may contain only letters, numbers, spaces, and the characters. _ - ( ). The server returns HTTP 400 for invalid names across all cluster-group, cluster, and server-connection create and update endpoints, and the client shows an inline validation error and blocks the save. (#269) -
Fix the alerter intermittently and silently suppressing anomaly alerts that should have fired. The Tier 3 LLM-based classifier parsed model responses non-deterministically, so the same response could resolve to either "alert" or "suppress" from one run to the next. The parser now strips markdown code fences and extracts an embedded JSON object before falling back to keyword matching, and the keyword fallback applies a deterministic, fail-safe precedence (alert before suppress, keep before clear) instead of iterating a randomly ordered map. An over-broad suppress keyword that matched phrases such as "deviation from normal behavior" was also tightened. The same anomaly condition now produces the same decision every time, so genuine alerts fire reliably. (#264)
-
Fix the Add Cluster Group, Add Cluster, and Add Server dialogs accepting Name, Host, Maintenance Database, and Username values longer than 255 characters, which the database rejected through its
VARCHAR(255)columns and surfaced as a generic, unhelpful server error. The web client now caps those fields at 255 characters so over-length input cannot be submitted, and the server validates their length and returns a clear 400 response (for example "Name must be 255 characters or less") instead of a generic 500. The server counts characters rather than bytes to match theVARCHAR(255)limit. (#270) - Fix Ellie, the AI chat assistant, hanging in an effectively
infinite tool-validation loop for regular, non-superuser
users. When a regular user selected a connection shared with
them and asked Ellie to describe it, the query-validation
tool kept succeeding while the tools that execute the query
failed with an access error. The assistant retried slightly
different queries indefinitely, repeatedly showing
Validating queryand never responding. The chat loop now applies a repeated-failure circuit breaker: when the same tool fails with the same error three times, the assistant stops retrying and returns a clear message. The message names the failing tool, shows the underlying error, and suggests a likely permissions or connection-access problem to raise with an administrator. (#268)
Security
- Log a warning recommending
chmod 600when reading any secret file from a group- or world-readable path. The read still succeeds, so existing deployments continue to work while operators tighten the affected file permissions. (#267)
[1.0.0-beta3] - 2026-05-26
Added
-
Add a hybrid variance floor and a per-
period_typewarmup gate to the alerter's Tier 1 anomaly detector, plus a symmetric z-score cap as defence in depth. Three new YAML blocks underanomaly.tier1expose the controls:max_z_score(default100.0, set to0to disable the clamp),variance_floorwithrelative_pct(default0.05) andabsolute_floor(default0.001), andwarmupwithmin_samplesandmin_span_hourspairs forall,hourly, anddailybaselines. A newmetric_baselines.earliest_sample_atcolumn is added by an idempotentALTER TABLEin the consolidated collector schema migration; the column is populated automatically by the alerter's baseline builder at the next refresh, and the warmup gate fails closed on NULL values until the column is populated. Theanomaly_candidatesandalertstables are deliberately left untouched to preserve audit history. Operational: operators upgrading an existing deployment mustTRUNCATE TABLE metric_baselinesafter stopping the alerter and before starting the new binary, so the detector rebuilds baselines under the new logic from a clean slate. -
Add a startup datastore schema health check to the MCP server. The server now reads the collector-owned
schema_versiontable and probes a small set of critical dashboard tables before wiring any handlers; a missing or pre-v4 collector schema, or a partial drop of any probed relation, surfaces an operator-actionable error that names the affected datastore and the recommended action, then exits non-zero. This replaces the previous silent failure mode where the server would come up against an empty database and 500 on every dashboard endpoint. -
Add the
get_timeline_eventsMCP tool, which exposes the unified incident-investigation timeline that was previously only reachable through the/api/v1/timeline/eventsREST endpoint. The tool returns configuration changes, HBA and ident edits, server restarts, extension changes, alert fired/cleared/acknowledged events, and blackout start/end markers in a single TSV result ordered most recent first. Ellie and other MCP clients can now correlate alerts with the underlying changes that may have caused them without falling back to direct REST calls. The tool gates results through the same RBAC checks asget_alert_history. (#250)
Security
- Fix the server creating the SQLite
auth.dbfile with world-readable mode0644on first start, exposing bcrypt password hashes and token hashes to other local users on the host. The previousos.Chmod(0600)call ran before themodernc.org/sqlitedriver had created the file on disk, so the chmod silently failed with ENOENT and the file was left at the operating system's umask default. The fix moves the chmod after thePRAGMA foreign_keysquery, which forces the driver to round-trip and create the file, and adds a post-chmodos.Statverification that logs a warning if the resulting mode is not0600. The server applies the chmod unconditionally on every startup, so an existingauth.dbleft at0644by an earlier release is re-narrowed automatically and no manual operator action is required. On filesystems that silently ignore chmod (for example FAT or some FUSE mounts), the server now refuses to start and operators should consult the WARNING log line that reports the observed mode. (#249)
Added
- Add Gemini as a supported embedding provider in the
embeddingandknowledgebaseserver configurations; the provider supportstext-embedding-004(default) andembedding-001, both at 768 dimensions. (#246)
Changed
- Update the default Gemini chat model in the alerter
and server configurations from
gemini-2.0-flash, which is no longer available to new API users, togemini-2.5-flash. (#246)
Fixed
-
Fix the
get_timeline_eventsMCP tool emitting a flat "Alert condition no longer active" summary onalert_clearedrows, which left a reviewer scanning the timeline to read the vividalert_firedsummary (carrying the alert's frozendescriptiontext) and risk misreading an already resolved alert as still active. Cleared rows now carry a self-contained summary of the formResolved after <duration>. Fired: <original alert description>, where<duration>renders asNs,Nm Ms, orNh Mm; the underlyingalerts.descriptioncolumn is preserved unchanged, so this is a presentation change in the timeline tool rather than a rewrite of historical alert text. The tool description visible to MCP clients now also states thatalert_fired.summarydescribes the firing condition and not the current state, and instructs clients to pair eachalert_firedrow with its correspondingalert_clearedrow (matching title prefix'Alert Cleared: ') before treating an alert as ongoing. -
Fix the collector entering a restart loop when its consolidated schema migration ran against a partially populated datastore. PostgreSQL has no
ALTER TABLE ... ADD CONSTRAINT IF NOT EXISTS, so a re-run that found a pre-existing foreign key (such asfk_anomaly_candidates_embeddingonanomaly_candidates) raisedduplicate_object, aborted the surroundingpgx.Tx, and the next statement returned SQLSTATE 25P02 ("current transaction is aborted, commands ignored until end of transaction block") with a misleading error pointing at an unrelated table. The migration now guards everyADD CONSTRAINTwith a catalog check againstpg_constraint, runs the optional pgvector blocks inside SAVEPOINTs so a missing extension or schema drift cannot poison the parent transaction, and surfaces real failures with areturninstead of swallowing them throughlogger.Infof. Fresh installs are unaffected; the change matters only for recovery cases. -
Fix the web client crashing into the "Something went wrong" error boundary after the user deleted an empty cluster. The server marshaled an empty
clusterslist (and the nestedserverslist inside each cluster) as JSONnullrather than[], andMainLayout's selectionuseMemothen called.someon the null value without a guard. The server'sGetClusterTopologynow normalises both lists to a non-nil empty array, and the client's selection logic guards every read ofgroup.clustersthrough a newbuildSelectionhelper. TheClusterGroup.clustersTypeScript type is also tightened toClusterEntry[] | nullso this regression class is caught by the type system. (#242)
[1.0.0-beta2] - 2026-05-14
Added
- Document the TLS and reverse-proxy requirements for
any network-accessible deployment in a new
Administrator's Guide page at
docs/admin-guide/tls-and-reverse-proxy.md; the page states explicitly that TLS termination, HTTP to HTTPS redirection, and HSTS are operator responsibilities at the reverse proxy layer, calls out the Vite dev server on port 5173 as localhost-only and unsupported for any network-accessible use, notes that the server's built-in TLS support remains available for operators who choose to terminate at the application, and enumerates the credential-exposure risks of running the workbench over plain HTTP. Cross-reference callouts now appear in the installation, quick-start, Docker, and web-client configuration pages. (#234) - Add a Playwright-based end-to-end smoke-test suite
that drives the production client bundle in a real
browser against a real server and Postgres on every
pull request; the suite runs across a
Chromium/Firefox/WebKit matrix and is invoked
locally with
make test-e2e. Seedocs/developer-guide/e2e/index.mdfor details. (#236) - Capture Go integration coverage from the running server during the end-to-end suite and merge it into the existing Codacy partial-upload pipeline, so unit and integration coverage combine into a single reported figure. (#236)
- Add the
spock_exception_logandspock_resolutionscollector probes; both probes capture a rolling 15-minute window of the Spock extension's exception and conflict-resolution catalogs and no-op cleanly on databases without Spock installed. (#200) - Add six built-in alert rules in the
replicationcategory:spock_recent_exceptions_present,spock_recent_exceptions_high,spock_recent_resolutions_present,spock_recent_resolutions_high,replication_slot_retention_warn, andreplication_slot_retention_high; the Spock rules require thespockextension and the slot retention rules apply to every PostgreSQL deployment. (#200)
Changed
- Breaking change: the web client container now
runs as a non-root user and listens on port
8080 instead of port 80. The base image in
client/Dockerfileswitched fromnginx:stable-alpinetonginxinc/nginx-unprivileged:stable-alpine, with an explicitUSER nginxdirective. Host-side port mappings indocker-compose.yml,docker-compose.prod.yml, and the walkthrough compose file are unchanged, sohttp://localhost:3000continues to work with the defaultCLIENT_PORT. Operators running custom reverse-proxy configurations, Kubernetes manifests, or externalproxy_passupstreams that target container port 80 must update those references to 8080; this includes ServicetargetPortvalues, health probes, and any direct container-to-container references. -
Breaking change: the collector, alerter, and server no longer auto-discover configuration or secret files in the binary directory or the current working directory; review the migration steps below before upgrading. (#195)
- The new lookup order is the
--configflag, the per-user config directory, and/etc/pgedge/; the first match wins, and missing files fall through to compiled-in defaults. - The per-user path resolves to
~/.config/pgedge/<binary>.yamlon Linux (honouring$XDG_CONFIG_HOME),~/Library/Application Support/pgedge/<binary>.yamlon macOS, and%AppData%\pgedge\<binary>.yamlon Windows. - The same precedence applies to the collector and
server secret files (
ai-dba-collector.secretandai-dba-server.secret); the alerter does not use a secret file. - Production deployments that already use
/etc/pgedge/are unaffected. - Development setups that drop a YAML file next to
the binary or in the current working directory
will silently fall through to compiled-in
defaults; move the file to
/etc/pgedge/or the per-user directory, or pass--configwith an explicit path. - The alerter's
SIGHUPhandler re-runs discovery on each reload, so installing a config at a default location after startup is picked up on the next signal. - Replace the composition-rule password validator with a policy aligned to NIST SP 800-63B; the server now requires a minimum of 12 characters, enforces the 72-byte bcrypt upper bound, drops uppercase, lowercase, digit, and special-character requirements, and rejects passwords found in a built-in dictionary of approximately 10,000 common and breached entries. The web client shows live password-strength feedback as the user types, and the server remains the authoritative validator. (#177)
- Document installation paths for each deployment
method (GitHub release, Docker, RPM/DEB) in the
installation guide with a reference table. Add
cross-reference notes to the quick start, Docker,
and sub-project README files. Align manual-install
systemd service names to
pgedge-ai-dba-*to match RPM/DEB package conventions. (#173) - Reject
cors_origin: "*"at server startup when authentication is enabled. Browsers discard credentialed responses that combineAccess-Control-Allow-Origin: *withAccess-Control-Allow-Credentials: trueper the Fetch spec. Operators should configure an explicit origin or leave the option empty for same-origin deployments. (#81) - Migrate the
collector,alerter, andserver.golangci.ymlconfigurations to the golangci-lint v2 format, and update the CI workflows to installgolangci-lint/v2;make test-allnow works again on developer machines that have golangci-lint v2 installed locally. (#66) - Apply a Biome and ESLint auto-fix pass across
client/src/, clearing roughly 600 Codacy findings across 294 files; the change is a mechanical refactor with no behavior changes, and existing lint and test baselines remain unchanged. - Clear all
@typescript-eslint/no-confusing-void-expressionfindings inclient/src/across 80 files; ESLint's auto-fixer resolved 279 sites and 19 remaining cases were rewritten manually by expanding() => cond && voidFn()into explicitifblocks. No behavior changes, and all 2,604 Vitest tests pass. - Raise line coverage of
server/internal/cryptofrom 86.8% to 100%. New tests cover four previously uncovered error branches. The branches are random source failure,ReadFilefailure,WriteFilefailure, and the GCM encrypt failure path. (#78) - Add integration tests for
server/internal/memory.Storeagainst PostgreSQL. The tests cover all nine public methods:NewStore,Store,Search,GetPinned,ListByUser,GetByID,Delete,DeleteByID, andUpdatePinned. They also exercise pgvector similarity ordering, scope visibility, and ownership checks. Package line coverage for the memory store now reaches 92.5%. (#78) - Add the
-raceflag to thetestandcoverageMakefile targets in theserver,collector, andalertersub-projects. The race detector now runs in CI and on developer machines. (#78) - Auto-collapse the Server Dashboard's "System
Resources" section when its data is unavailable,
typically because the
system_statsPostgreSQL extension is not installed on the connected server; the section previously stayed expanded and rendered five empty CPU, Memory, Disk, Load, and Network IO panels that pushed the "PostgreSQL Overview" section far down the page. The collapsed header now shows the italic message "No data available. Is the system_stats extension installed?" next to the title, and the user can still expand the section manually to inspect the empty panels. The manual override is intentionally not persisted tolocalStorage, so the section returns to the user's previous expand or collapse preference once the extension is installed. The sharedCollapsibleSectioncomponent gained two new props,forceCollapsedandforceCollapsedMessage, which temporarily override the persisted state without mutating storage and render the italic header message; an anti-flicker guard delays the force-collapsed state until the initial KPI fetch completes, so the section does not briefly collapse during loading. - Bump the Go toolchain from 1.26.1 to 1.26.2
across the server, collector, alerter, and
pkgmodules and the dev-container image; the upgrade picks up upstream fixes for seven Go security advisories listed in the Security section. - Bump
github.com/jackc/pgx/v5from 5.7.6 to 5.9.2 in the server, collector, and alerter; the upgrade picks up the memory-safety and dollar-quoted-string fixes listed in the Security section. - Add a
.codacy.yamlconfiguration that suppresses confirmed false-positive findings from Codacy's Semgrep and ESLint8 engines; suppressions are scoped to specific files or to__tests__/**globs, were independently reviewed by the security-auditor agent, and mask no real vulnerabilities. - Reduce the web client's default
body1andbody2typography sizes to the MUI standards of 16px and 14px, and remove the roughly 80 inlinefontSizeoverrides that previously compensated for the oversized defaults; body text across the application now renders smaller and more consistently, while headings and subtitles are unchanged. - Add a
CHART_AXIS_LABEL_FONTSIZEtoken inclient/src/theme/tokens.tsso chart axis labels render at a consistent 14px, and add aMONO_CAPTION_SXtoken that deduplicates the monospace caption styling shared by alert thresholds, Spock node info, and cron schedules. - Consolidate four duplicated patterns in
server/srcas part of the codebase cleanup tracked in #77. The copy-pastedgetClient()helper ininternal/tools/context_aware_provider.goandinternal/resources/context_aware_registry.gonow delegates to a new(*database.ClientResolver).ResolveOrErrormethod that returns the canonical "no database connection configured" error. The 14-fieldchat.NewClientFromConfiginvocation repeated ininternal/llmproxy/proxy.go(HandleModelsandHandleChat) andinternal/overview/generator.go(createLLMClient) collapses into a newchat.NewClientFromLLMConfigfactory that takes anLLMOptionsparameter for per-call overrides such asModel,MaxTokens,Temperature,Debug, andHeaders, removing roughly 40 lines of boilerplate per call site. The twoauth.ConnectionVisibilityListeradapters ininternal/api/helpers.goandinternal/database/visibility_lister.goshare a single projection; the slice-based adapter moves into thedatabasepackage asdatabase.NewSliceVisibilityListerand the projection is exported asdatabase.ConnectionsToVisibilityInfo. The five-line closure that wired(*database.Datastore).GetConnectionSharingInfointo anauth.RBACCheckerfrominternal/tools/context_aware_provider.go,internal/resources/context_aware_registry.go, andcmd/mcp-server/handlers.gonow flows through a newauth.NewRBACCheckerForDatastoreconstructor that accepts the datastore through a smallDatastoreSharingLookupinterface and handles nil and typed-nil cases internally. The change is internal-only and behavior-preserving; no public HTTP API, MCP tool, or configuration surface changes. (#77)
- The new lookup order is the
Security
- Pick up upstream fixes for seven Go security
advisories by bumping the toolchain to 1.26.2;
the advisories are CVE-2026-32280 (certificate
chain validation denial of service), CVE-2026-33810
(DNS-constraint certificate validation bypass),
CVE-2026-32281 (certificate chain validation denial
of service), CVE-2026-32283 (TLS 1.3 key-update
denial of service), CVE-2026-32289 (
html/templatecross-site scripting), CVE-2026-32288 (archive/tardenial of service), and CVE-2026-32282 (Root.Chmodsymlink escape). - Pick up upstream fixes in
github.com/jackc/pgx/v5by bumping to 5.9.2; the advisories are CVE-2026-33816 (Critical, memory safety) and GHSA-j88v-2chj-qfwx (Low, SQL injection through dollar-quoted-string and$Nplaceholder confusion). A code audit confirmed that no query in this project mixes$$...$$literals with$Nplaceholders, so the second advisory was theoretical for our code base; the bump is still warranted as a defence-in-depth measure. - Bump the web client container's base images to
pick up upstream fixes for high-severity CVEs
flagged by Docker Scout. The builder stage moves
from
node:22-slimtonode:22-trixie-slim(Debian 13), which closes CVE-2026-33845 and CVE-2026-33846 ingnutls28. The runtime stage moves fromnginxinc/nginx-unprivileged:stable-alpinetonginxinc/nginx-unprivileged:stable-alpine-slim, which closes CVE-2026-3805 incurlon Alpine 3.23; the slim variant omitscurl, which the runtime does not need. One residual high finding, CVE-2026-33671 in thepicomatchpackage bundled inside npm, persists across all Node 22 and 24 tags pending an npm release; the residual lives only in the builder stage and never reaches the shipped image. The non-root UID 101 nginx user, port 8080, and other hardening from the earlier base-image change are preserved, anddocker scout cvesreports no vulnerable packages in the final image. -
Redact notification channel secrets from API responses;
GET /api/v1/notification-channelsandGET /api/v1/notification-channels/{id}no longer returnsmtp_username,smtp_password,webhook_url, orauth_credentials, all of which were previously emitted in plaintext after server-side decryption. Each response now includes the boolean indicatorssmtp_username_set,smtp_password_set,webhook_url_set, andauth_credentials_setso clients can show whether a secret is configured without ever reading the value. ThePUT /api/v1/notification-channels/{id}endpoint applies a three-way merge to the four secret fields; omit a field to preserve the stored value, send an empty string to clear it, or send a new value to replace it. The web admin UI for the Email, Slack and Mattermost, and Webhook channel editors now leaves secret form fields blank when editing an existing channel and preserves the stored value unless the user types a replacement. (#187) -
Fix log, SQL, and SMTP injection findings surfaced by the golangci-lint v2 upgrade; the knowledgebase search now binds its filter values through
?placeholders instead of string concatenation, the email test sender sanitizes envelope and header fields before writing them to the SMTP connection, and user-tainted values are escaped through a newlogging.SanitizeForLoghelper at log sites across the api, auth, and config packages. (#66) -
Hoist RBAC access-control checks above all datastore calls in the alert-counts, alert acknowledgement, alert unacknowledgement, alert analysis, and cluster-topology handlers; zero-grant callers now short-circuit to an empty response without touching the database, and HTTP-level regression tests cover every affected handler. (#67)
-
Extend the
manage_connectionsgate to the remaining connection paths missed by the earlier sweep under #207; the server'screateConnectionhandler now requires the permission for every new connection rather than only for shared connections, thehandleUpdateConnectionClusterhandler behindPUT /api/v1/connections/{id}/clusternow also requires the permission so that a user with only read-visibility on a connection can no longer re-home it between clusters, and the web client's Add menu hides the "Add Server" entry from users who lack the permission. The new server-side gate sits after the existing visibility check and before any body decode or datastore mutation. Unauthorized callers receive a403 Forbiddenresponse with a clear authorization error, and the OpenAPI specification and the staticdocs/admin-guide/api/openapi.jsondocument the new response on the affected paths; the previously undocumented/connections/{id}/clusterpath is now present in the specification with both its GET and PUT methods. (#233) -
Require the
manage_connectionspermission on all cluster and cluster-group mutating endpoints; users without the permission previously could create cluster groups and clusters through the REST API, and the server silently committed the rows and returned a success status even though the resulting records were invisible to the creator while remaining visible to administrators. A follow-up audit found the same gap on additional mutating routes, so the fix now gates eleven endpoints in total: creating and deleting cluster groups, adding clusters to a group, creating, updating, and deleting clusters, adding and removing cluster servers, and creating, updating, and deleting cluster relationships. Unauthorized callers now receive a403 Forbiddenresponse with a clear authorization error instead of a misleading success, and the group owner can still delete a group they own even without the permission. The web client's Add menu now hides the "Add Cluster Group" and "Add Cluster" entries from users who lackmanage_connections, and the OpenAPI specification and staticdocs/admin-guide/api/openapi.jsondocument the403 Forbiddenresponse on the affected paths. Administrators see no functional change. (#207)
Fixed
- Fix the Replication Type select in the Cluster Settings
dialog rendering blank for auto-detected clusters whose
replication_typecolumn is NULL; the dialog now derives the value from the cluster'sauto_cluster_keyprefix when no explicit replication type is set, mirroring the existing behaviour of the Topology panel. (#235) - Fix auto-detected clusters lingering in the cluster
autocomplete dropdown after their last member connection
was deleted;
DeleteConnectionnow runs in a single transaction that marks the parent clusterdismissed=TRUEwhen the cluster was auto-detected (auto_cluster_key IS NOT NULL) and no connections still reference it. The delete itself usesDELETE ... RETURNING cluster_idto close a TOCTOU window between the lookup and the delete, and the rollback path usescontext.Background()to avoid a pgx v5 close-of-closed-channel panic when the request context is canceled mid-transaction. User-created clusters and connections without a cluster assignment are unaffected. (#245) - Fix anomaly alerts displaying raw dotted metric names such
as
pg_replication_slots.max_retained_bytesin the Status Panel, and six alert rules seeded by collector migration v3 rendering as auto-title-cased fallbacks in the Admin Alert Rules panel; theFRIENDLY_ALERT_TITLESmap now includes curated labels for 15 dotted anomaly metric names and the 6 v3 rule names, with full test coverage of the map. (#244) - Fix long, non-wrapping SQL queries flowing underneath the copy and run icons in the Remediation Steps panel of the alert AI analysis view; the shared markdown styles now reserve right-side padding on each code block sized to the number of overlaid action buttons, so the SQL text never collides with the icons. (#221)
-
Fix the Active Alerts Restore button returning HTTP 500 "Failed to unacknowledge alert" for alerts that were already non-acknowledged (for example after the alerter reactivated them following a severity change); the server now maps a missing alert to 404, an alert that is not currently acknowledged to 409 Conflict, and wraps every failure path with the alert ID for actionable logs. The alerter's auto-reactivation path is also hardened against panicking on alerts with a NULL
metric_valuecolumn and captures the previous severity before the database write so the in-memory comparison cannot drift from the acknowledged state. (#227) -
Fix the alerter's
replication_slot_inactivecritical alert never firing because its metric query selected from a non-existentmetrics.pg_stat_replication_slotstable; thepg_replication_slots.inactivemetric now reads directly frommetrics.pg_replication_slots(the table the collector probe writes to) and derives the inactive state from theactivecolumn. New integration tests cover the happy path, the no-row case when every slot is active, slot deduplication per connection, and the 5-minute freshness cutoff. (#224) -
Fix Ask Ellie incorrectly reporting missing Spock replication slots on healthy Spock 6.x clusters; the assistant previously generated
WHERE plugin = 'spock'againstpg_replication_slots, but current Spock releases name the output pluginspock_output. The chat system prompt inserver/src/internal/chat/llm.gonow instructs Ellie to useplugin LIKE 'spock%'for cross-version compatibility. (#220) -
Fix three datastore schema and probe inefficiencies identified during a production performance review. Collector migration v4 adds a partial index on
anomaly_candidates(detected_at)filtered toprocessed_at IS NULL AND tier1_pass = TRUEso the alerter sweeper stops sequential-scanning the full table on every poll, and drops the redundantidx_pg_stat_all_indexes_conn_db_timeandidx_pg_stat_statements_conn_db_timeindexes (along with their attached child indexes on every existing weekly partition) which were duplicated by the more specific_objectindexes. The change-detection probes (pg_settings,pg_extension,pg_hba_file_rules,pg_ident_file_mappings) now strip theai_dba_wb_probemarker column injected byWrapQuerybefore hashing, so the live snapshot hash matches the stored snapshot hash; previously the marker caused every hourly collection to look "changed" and write a fresh snapshot, inflating thepg_settingspartitions by roughly an order of magnitude. (#219) -
Fix the Admin panels showing a success toast alongside a page-level refresh error when a save succeeded but the follow-on reload failed; the shared
useCrudPanelhook now suppresses the success toast when the post-mutation refresh fails, so the user sees only the actionable refresh error. (#215) -
Fix the
AdminMessagingChannelspanel flipping thedeleteLoadingbusy flag when a user toggled a channel's enabled state; the shareduseCrudPanel.runMutationhelper gained an independentbusyTargetoption ('save' | 'delete' | 'inline') that decouples the busy-state flag from theerrorTarget, and a new'inline'error target routes errors only to the caller through the returned result rather than tocrud.errororcrud.dialogError. Defaults preserve the previous behavior so existing call sites are unchanged, andhandleToggleEnablednow usesbusyTarget: 'inline'. (#216) - Fix the divergent error fallback wording shown by the
Admin panels when a thrown value is not an
Errorinstance. TheAdminUsers,AdminMemories,AdminTokenScopes,AdminPermissions,AdminProbes,AdminEmailChannels,AdminWebhookChannels,AdminMessagingChannels, andAdminGroupspanels, along with theuseChannelCRUDhook, now route non-Errorthrows through the sharedextractErrorMessagehelper inclient/src/components/AdminPanel/_shared/errors.ts. The helper returns the generic'An unexpected error occurred'message instead of leaking output such as[object Object]produced byString(err). Panels that pass a contextual fallback (for example, "Failed to add recipient") to the helper's second argument retain that context-specific wording. (#212) - Fix the
Chart.test.tsxregression introduced in commitaa28aa8that has been failing the CI - Client workflow on every commit since; the vitest mock specifierecharts-for-react/lib/corewas not updated toecharts-for-react/esm/corewhen production code switched to the ESM path, leaving the realReactEChartsCorerunning in tests and tripping the deliberately-narrowecharts/coremock. The change updates only the test mock specifier; no production source code was modified. - Fix the npm-install branch in
start_dev_web_client.shnever firing because an interveningechoclobbered$?before theif [ $? -eq 0 ]check; the script now uses a direct&&/||pattern that tests the previous command's exit status without an intermediate command. - Fix Ask Ellie entering a long retry loop ("Joining the relations..." / "Validating query") when the signed-in user has no MCP privileges; the chat now surfaces a clear permission-denied message immediately instead of cycling through planning steps. (#188)
- Fix wide markdown tables overflowing and clipping the
right-side columns inside the Ask Ellie chat panel;
tables returned by MCP tools such as
get_alert_history,get_alert_rules, andquery_datastorenow sit inside a horizontally scrollable wrapper, so narrow tables still fill the bubble while wide tables scroll within it instead of spilling outside. The sharedMarkdownContenthelper used by other dialogs received the same treatment. (#185) - Fix the web client rendering a blank screen on every
navigation when the LLM proxy was enabled with a
reasoning model that returns a structured
summaryobject;AIOverviewnow coerces non-string summaries to text before rendering, removing the React error that triggered the blank screen. The top-level<ErrorBoundary>has also been rewritten to always show the error message and component stack in a collapsible details block and to expose a "Reload" button, so users can recover from a crash and file actionable bug reports without rebuilding the container. (#182) - Fix stale auto-detected edges remaining in
cluster_node_relationshipsafter the cluster topology changed through failover, subscriber removal, or a new parent in a binary chain;SyncAutoDetectedRelationshipsnow replaces the auto-detected set transactionally, deleting all existingis_auto_detected = TRUErows for the cluster before inserting the freshly detected set, and thesyncRelationshipsFromTopologycaller no longer short-circuits on an empty detected slice. Manual relationships and auto-detected rows for other clusters are preserved, and a failure during the delete or insert rolls the transaction back to the prior state. (#152) - Fix the cluster Topology tab dropping cascading standbys and marking empty auto-detected nodes as expandable; persisted and manual chains such as primary -> standby -> cascading standby now render every level regardless of input order, and nodes whose children are filtered out no longer display a disclosure arrow. (#153)
-
Fix the collector probe config loader ignoring scope and silently re-enabling disabled parent overrides;
LoadProbeConfigsnow restricts its query toscope IN ('global', 'server')so cluster- and group-scoped rows no longer collapse into theconnection_id = 0bucket and get misapplied as global defaults, andEnsureProbeConfignow inherits the parent config'sis_enabledvalue when materializing a server-level row instead of hard-coding it to true. The SQL is extracted into aloadProbeConfigsQueryconstant and the value resolution moves into a pureresolveProbeConfigDefaultshelper, both covered by new unit tests. (#151) -
Fix MCP and admin scope privileges granted through a wildcard group grant (
"*") being silently dropped during token scope intersection; the intersection logic now recognises wildcard grants and preserves the scoped privileges. The fix also introduces an explicitAccessLevelNoneconstant to replace raw empty strings for "no access" semantics, improving code clarity and reducing error-prone comparisons. (#96) -
Fix the ClusterNavigator group-editing flow round-tripping string group ids (
"group-{x}") through numeric parses; the string id now travels unchanged throughhandleConfigureGroup,handleSaveGroup, and the cluster actions context, and the onestrconv.Atoi-compatible conversion happens at theGroupDialogoverride-panel boundary. Auto-detected groups without a numeric backing row now display an info alert explaining that alert, probe, and channel overrides are unavailable instead of silently passingNaNto the override panels. This removes the root cause patched symptomatically in #59. (#63)
[1.0.0-beta1] - 2026-04-21
Added
- Add the
llm.timeout_secondsserver configuration option to control the HTTP client timeout for requests to the configured LLM provider; the default remains 120 seconds. (#60) - Add a guided walkthrough example with pre-seeded demo data and an in-browser Driver.js tour covering the workbench's major features.
Changed
- Default the knowledgebase
database_pathto the pgEdge package install path at/usr/share/pgedge/postgres-mcp-kb/kb.db. (#52)
Fixed
- Remove misleading raw API key options from the
example server configuration files; the server
only accepts API keys through the corresponding
*_filevariants. (#54) - Fix spurious "partition would overlap" errors from the collector on non-UTC hosts when the weekly partition rolled over. (#55)
- Fix foreign key violations during alerter baseline calculation when metric rows outlive their connection; historical metric queries now filter through the connections table. (#56)
- Fix MCP tool invocations failing with TLS
certificate verification errors against servers
that require a custom
sslrootcert,sslcert, orsslkey; the server now forwards these fields on the database connection string. (#57) - Fix reactivated alerts continuing to appear as
acknowledged in the GUI by clearing stale
alert_acknowledgmentsrows when an alert leaves the acknowledged state; the alerts API now also exposes a nullablelast_updatedRFC3339 timestamp and the StatusPanel surfaces it alongside "Triggered" when the two differ. (#64) - Fix the cluster Topology tab "Add server" dropdown
silently excluding servers that had been re-claimed
by an auto-detected Spock cluster; the connections
API now returns the
membership_sourcefield the client filter requires. (#25, #46) - Fix dismissed auto-detected clusters reappearing
after the collector's next auto-detection run;
UpsertAutoDetectedClusterno longer clears thedismissedflag on rediscovery, andGetClusternow filters dismissed rows from single-cluster lookups. (#36) - Fix dismissed auto-detected clusters reappearing in the Server creation dialog's cluster dropdown after alert or connection context was fetched; the connection hierarchy resolver now skips dismissed rows and no longer resurrects them through its upsert fallback. (#36)
- Fix partitions not being dropped at the appropriate time by the collector. (#62)
- Fix the copy-to-clipboard button in the Admin Tokens "Token created" dialog; the button now shows a check mark and "Copied!" tooltip on success and surfaces clipboard failures through the error alert. (#71)
- Fix the StatusPanel "Restore to active" action silently failing on error; the alert now leaves the acknowledged list optimistically, rolls back and surfaces a Snackbar error on API failure, and guards against double-click submissions. (#72)
- Fix servers assigned to a manually created cluster
continuing to appear under a re-created
auto-detected cluster after the next topology
refresh; auto-detected Spock, binary-replication,
and logical-replication grouping now skip
connections with
membership_source = 'manual'. (#74) - Fix
GET /api/v1/connectionsreturning an empty array for scoped API tokens when the token owner's read access came from a wildcard group grant; the scoped connections are now returned as expected, and token scopes continue to restrict but not elevate the owner's privileges. (#83)
Security
- Fix several REST endpoints and MCP tools leaking
unshared connections owned by other users; the
connection detail, database listing, connection
context, cluster topology, alerts, alert
acknowledgement, alert analysis, timeline, and
current-connection endpoints now apply the same
ownership, sharing, group, and token-scope checks
as
GET /api/v1/connections, returning403 Forbiddenfor single-resource requests and filtering list responses to the caller's visible connections. Theget_alert_history,get_metric_baselines, andget_blackoutsMCP tools applied no connection filter for callers with zero explicit grants; all three now restrict results to connections the caller is permitted to see. The OpenAPI specification and the staticdocs/admin-guide/api/openapi.jsonnow document the403 Forbiddenresponse on the affected single-resource endpoints. (#35) - Fix server and cluster visibility leaks through the
cluster list, cluster group, and cluster-membership
REST endpoints;
GET /api/v1/clusters/list,GET /api/v1/clusters/{id},GET /api/v1/clusters/{id}/servers,GET /api/v1/cluster-groups, andGET /api/v1/cluster-groups/{id}now filter clusters, groups, and member servers to the caller's visible connections and return404 Not Foundfor clusters or groups that contain no visible members. (#35) - Fix additional RBAC leaks surfaced by the follow-up
security audit; the overview REST and SSE endpoints
(
GET /api/v1/overviewandGET /api/v1/overview/stream) now verify scope visibility for scoped requests and filterconnection_idslists to the caller's visible set; the blackout list and get endpoints (GET /api/v1/blackoutsandGET /api/v1/blackouts/{id}) hide blackouts whose referenced connection, cluster, or group is not visible to the caller; the alert, probe, and channel override list endpoints (GET /api/v1/alert-overrides/{scope}/{scopeId},GET /api/v1/probe-overrides/{scope}/{scopeId}, andGET /api/v1/channel-overrides/{scope}/{scopeId}) return404 Not Foundwhen the caller cannot see the requested scope; and the MCP connection resolver now checks RBAC before loading credentials and returns a generic "connection not found or not accessible" message for both missing and denied cases. (#35) - Fix remaining RBAC leaks flagged by the third-round
security audit; the
query_metricsandget_alert_rulesMCP tools now checkCanAccessConnectionbefore any datastore read and return a generic "connection not found or not accessible" error for both missing and denied connections, closing the ID/name enumeration that the previous error path exposed. The overview scoped-snapshot path (GET /api/v1/overview?scope_type=cluster|group) now intersects the scope's member connection IDs with the caller's visible set and generates the summary from the intersection through the existing connections- summary path, so two callers with different visibility never share a cache entry; scope denial now returns404 Not Foundto match sibling handlers. The blackout schedule list and get endpoints (GET /api/v1/blackout-schedulesandGET /api/v1/blackout-schedules/{id}) apply the same visibility filter as the blackout endpoints, and the alert override context endpoint (GET /api/v1/alert-overrides/context/{connectionId}/{ruleId}) now gates onscopeVisibleToCallerbefore reading override hierarchy. The MCP connection resolver logs RBAC denials so operators can correlate without widening the caller-visible surface. (#35)
[1.0.0-alpha3] - 2026-04-08
Added
- Add a Docker publish workflow that builds and pushes multi-platform images to GitHub Container Registry on version tags and main branch pushes.
- Add a production Docker Compose configuration using pre-built images with resource limits and log rotation.
- Add a Docker deployment guide to the documentation.
- Add a favicon to the web client.
- Replace the SQLite driver with a pure-Go implementation to support CGO-free builds.
Changed
- Improve blackout status indicators and require confirmation before deleting a blackout. (#34)
- Limit blackout scope options to relevant entries only. (#33)
- Allow servers from auto-detected clusters to be reassigned to manual clusters. (#46)
- Hide alert threshold links for users who lack the required permission. (#40)
- Display errors to users when fetching unassigned servers fails.
Fixed
- Fix the blackout dialog refreshing unnecessarily when underlying components update. (#47)
- Fix missing browser refresh after certain navigation actions and prevent title wrapping. (#45)
- Fix the replication type not carrying through to the edit dialog. (#44)
- Fix multiple potential crashes after a network failure and recovery. (#43)
- Fix a serialization error when updating server details. (#42)
- Fix a crash when clicking an empty cluster. (#39)
- Fix inconsistent alert operator values. (#38)
- Fix auto-detected clusters reappearing after deletion. (#36)
- Fix the is_shared flag for servers not being respected in all cases. (#35)
- Fix connection error alerts ignoring active blackouts. (#32)
- Fix MCP write access to databases. (#29)
- Fix SSL settings being silently dropped when creating or updating a server.
- Fix various issues with the database summary popup.
- Fix various TypeScript type safety issues in the web client.
Security
- Fix MCP memory tools bypassing RBAC checks; all authenticated users could access the datastore without proper authorization.
[1.0.0-alpha2] - 2026-03-04
Fixed
- Fix a crash in the Add Server dialog when no clusters exist in the database.
[1.0.0-alpha1] - 2026-03-02
Initial release.