Changelog
This page tracks notable platform changes — new features, behavior changes, security fixes, performance improvements, schema migrations, and deprecations. Entries are reverse chronological. For granular code history, see git logs in each repository.
2026
2026-07-30
Dashboard: stronger protection against HTML in displayed text
Wherever the dashboard shows a name, label, message, tooltip or hint that might contain HTML — for example a dataset or entity name, a notification or an alert — that content is now cleaned with the browser framework's built-in sanitiser before being shown. The previous approach relied on a hand-maintained list of dangerous patterns that could be worked around, in principle allowing crafted content to run scripts in another user's browser; the new approach reliably removes scripts, event handlers and unsafe links while keeping harmless formatting such as the bold highlighting of search matches. (Web App 2.39.0.)
Competitor Monitoring: cost price and richer filters on the products list
The products list now shows each of your products' cost price alongside its selling price, and can be filtered by product name and by profit segment, in addition to the existing brand, GTIN and price-segment filters. The "has matches" filter is now more precise: rather than a simple yes/no, you can show products whose match has been accepted, products with a pending (not-yet-reviewed) match, or products with no matches at all. Clicking a product opens a detail panel with its price history and the competitor matches found for it. (@attlaz/apps-lib 1.69.0, Products-Api 1.28.0, Competitor Monitoring service 1.11.0, Web App TBD.)
Competitor Monitoring: a fuller competitor view with matches and history
Opening a competitor now shows a detail view with separate Products and Matches tabs. The Matches tab lists the competitor's products matched against your catalogue and charts how many matches were discovered and how many you accepted or declined over time; while a matching run is in progress, the competitor header shows the match count climbing live. You can jump straight to a competitor by clicking its name anywhere in the Jobs view, and start a re-index or a matching run directly from the competitor's page. (@attlaz/apps-lib 1.69.0, Products-Api 1.28.0, Web App TBD.)
Competitor Monitoring: revisit accepted and declined matches, not just pending ones
The match review page now has two tabs: Pending, for matches awaiting your decision, and Reviewed, which lists the matches you have already accepted or declined and lets you change a past decision. Both tabs can be filtered by competitor, GTIN and product name. (@attlaz/apps-lib 1.69.0, Products-Api 1.28.0, Web App TBD.)
API: clearer, more consistent error responses
When a request fails, the error object in the response is now more precise. Validation errors that concern a specific field include a param field naming the request parameter at fault, so you can pinpoint the problem without reading the message text. The machine-readable code is now included only when a specific code applies, instead of being sent as an empty value. Unexpected server errors (500) always return a generic message — the underlying detail is recorded on our side and never exposed in the response. Separately, required text parameters now consistently reject an empty value with a 400 rather than sometimes accepting it. (@attlaz/api-lib TBD; live across the Attlaz API.)
2026-07-29
Dashboard: pages now have stable, shareable project URLs
Every page in the dashboard now carries the project — and, where it matters, the environment — in its web address. A link to a flow, connection, log stream, report or app therefore always opens in the right project, whether you bookmark it, paste it to a colleague, or follow it from an email. Switching project or environment updates the address to match, so what you see and the link you can share never disagree. Note: links saved under the previous URL format no longer open and should be re-saved from the current pages. (Web App 2.38.0.)
2026-07-28
Competitor Monitoring: product-matching and crawl jobs can be cancelled, with clearer statuses
Product-matching and crawl jobs can now be called off, and the Product Matching and Crawl Jobs pages show two new statuses to reflect it: Cancelled for a job that was stopped before it started, and Stopped for a job that was interrupted while running. Previously such jobs could be left showing as still running or lumped in with genuine failures; the distinction now makes it clear a job was deliberately ended rather than that something went wrong. (@attlaz/apps-lib TBD, Competitor Monitoring service TBD, Products-Api TBD, JavaScript SDK TBD, Web App TBD.)
Dashboard: consistent number formatting and a shared marker for empty values
Numbers across the dashboard — counts, quantities, prices and percentages — are now formatted consistently, with thousands grouped for readability (for example 1,234,567) and very large tallies optionally shown in short form (such as 12k or 1.5m). Where a value is missing or not yet available, the dashboard now shows a single consistent marker (an em dash) in place of the earlier mix of blanks and dashes. (Web App 2.37.0.)
2026-07-27
Dashboard: generated avatars stay perfectly round at every screen size
The automatically generated placeholder avatars could appear squashed into an oval at certain window widths — most visibly in lists like the competitor overview, where some avatars in a row stayed round while others next to longer names or web addresses were pinched. Avatars now hold their circular shape regardless of the space available, and a long name or address beside one is trimmed with an ellipsis instead of squeezing the avatar. (Web App 2.36.0.)
Dashboard: progress bars have a refreshed, consistent look
The progress bars shown for storage usage and for crawl and product-matching jobs have been restyled to a single consistent design across the dashboard, with the value shown alongside the bar. (Web App 2.36.0.)
2026-07-26
Competitor Monitoring: product matching no longer stalls or restarts on large catalogues
Matching a competitor's products against a large catalogue could leave the matching service unable to answer the platform's health checks while it was computing product similarities, which caused the platform to restart it mid-run — so a large matching job could keep starting over and never finish. The heavy similarity computation now runs to one side of the service's main loop, so the service stays responsive throughout and matching jobs complete reliably even for large catalogues. A job that is interrupted anyway — for example during a routine deployment — now shuts down cleanly and is automatically retried instead of being left showing as still running. (Competitor Monitoring service 1.4.2, Core 1.237.0.)
Competitor Monitoring: see how many matches each product-matching job found
The Product Matching page now shows, for every competitor's matching job, how many product matches it found — alongside the existing status and progress. The number appears as soon as matches start being recorded and keeps climbing while the job runs, so you can see at a glance how much overlap was found with each competitor. (@attlaz/apps-lib 1.54.0, Products-Api 1.14.2, Web App 2.35.0.)
Competitor Monitoring: crawls always finish, so follow-up processing isn't left hanging
A crawl could occasionally get stuck and never finish: if a fetched page couldn't be analysed — for example a page whose contents the parser couldn't read, or when an analysis step was interrupted — that page was left in limbo, so the crawl never reached completion and the follow-up processing that depends on it (price changes, matching, market positions) never started. Unreadable pages are now recorded as failed instead of stalling the crawl, and a periodic safeguard finishes any crawl whose pages have been left unresolved for too long, so a crawl always completes and its follow-up processing runs. (@attlaz/apps-lib 1.53.0, Competitor Monitoring service 1.4.2.)
Competitor Monitoring: post-crawl processing completes for very large catalogues
After a crawl, the background step that records price changes, marks products no longer seen as inactive, and recomputes market positions could fail outright on catalogues with tens of thousands of products — so for the largest catalogues that processing never completed. It now works through the catalogue in a streaming fashion instead of loading it all into memory at once, and deactivates no-longer-seen products in a single bulk operation, so it completes reliably regardless of catalogue size. (@attlaz/apps-lib 1.53.0, Competitor Monitoring service 1.4.2.)
Competitor Monitoring: the Product Matching list shows every job, in the right order
The Product Matching page could show only a couple of jobs, or list them in a jumbled order, because the underlying records weren't being sorted consistently when paging through them. The database now keeps these identifiers in a consistent order, so the page reliably lists every matching job — newest first — and pages through them correctly. (Database schema migration.)
2026-07-24
Competitor Monitoring: product matching shows the exact product count and progress from the start
While a competitor's product matching job was still queued or just getting under way, the Product Matching page showed no progress bar and no product count — the total only appeared partway through the run. Each matching job now records how many of the competitor's active products it will process the moment it is queued, so the count and a progress indicator are visible straight away and the bar fills in as products are matched. (@attlaz/apps-lib TBD, Competitor Monitoring service TBD, Web App.)
Competitor Monitoring: product matching shows progress immediately and runs competitors in parallel
When product matching is requested for a catalogue — from the Product Matching page or automatically after a crawl — each competitor now gets its own matching job that appears straight away, so you can watch matching advance competitor by competitor instead of waiting with no visible change until the whole run finishes. Each competitor is now matched independently, so the work can be spread across multiple workers and finishes faster for catalogues with many competitors, and if one competitor's matching fails it is retried on its own without holding up the rest. Re-requesting matching replaces the previous queued run rather than stacking a duplicate. (@attlaz/apps-lib 1.52.0, Competitor Monitoring service 1.3.0.)
Competitor Monitoring: no duplicate product matches, faster matching, and more resilient post-crawl processing
A product in your catalogue can no longer be linked to the same competitor product by more than one match — a uniqueness safeguard is now enforced in the database, so duplicates can't build up when matching runs for several competitors at once or a run is retried. Matches are also saved in batches rather than one at a time, so matching finishes faster on large catalogues. Separately, the background processing that follows a crawl — recording price changes, matching, tagging, refreshing the price index, and recomputing market positions — now retries automatically after a short delay when it hits a temporary problem such as a brief database interruption, instead of silently skipping that step. (@attlaz/apps-lib 1.52.0, Competitor Monitoring service 1.4.0; database schema migration.)
Competitor Monitoring: products with long web addresses or names now import, and one bad product no longer stops a scrape
Some competitor products were being rejected while a catalogue was scraped: those with a long product web address or a long product name exceeded a database field limit, which returned an error and stopped the entire scrape for that competitor — so the rest of that competitor's products didn't come through either. Product web addresses and names now accept the lengths real shops use, so these products import correctly. In addition, if a single product still can't be saved for any reason, it is now logged and skipped rather than aborting the whole run, so the remainder of the catalogue is still imported and the crawl finishes cleanly. (Database schema migration; Products-Api TBD.)
2026-07-23
Competitor Monitoring: follow each crawl with live progress, and "last scraped" is accurate again
Every time a competitor's catalogue is scraped is now recorded as a crawl job. A competitor's "Last scraped" time reflects its most recently completed crawl, and finishing a crawl automatically starts the follow-up processing — recording price changes, matching products against your catalogue, and refreshing the price index. From the dashboard you can open a competitor and choose "View crawl jobs" to watch a crawl in progress: each run shows its status and a progress bar that fills in as the competitor's pages are fetched and analysed. Which pages are products is now decided from each page's content rather than guessed from its address, so products are no longer missed because of an unusual URL. (Products-Api 1.14.0, @attlaz/apps-lib 1.51.0, @attlaz/client 1.88.0, Web App.)
JavaScript SDK: updates are applied, and flow-run arguments are sent unchanged
Two fixes to the JavaScript client. Update calls (sent as HTTP PATCH) were dropping their request body, so updating a record — for example a stored product — silently did nothing; updates now include their data and apply correctly. And when starting a flow run, argument names were being reformatted (for example sitemapUrl arriving as sitemap_url), so a flow could receive different argument names than were passed; flow-run arguments are now delivered exactly as provided. (@attlaz/client 1.86.0.)
Runners: create additional runner pools
You can now create additional runner pools within an environment, alongside the environment's default pool — useful for giving certain flows their own dedicated runner capacity. A new pool is created empty and is provisioned into the cluster when you deploy it (its runners and queue appear once that finishes). Its scaling and resource limits (min/max runners, CPU and memory) can be set when creating the pool or adjusted afterwards, and inherit from the environment by default. (Web App 2.35.0, Core-Api, @attlaz/client 1.86.0.)
Runners: queue statistics display correctly for new and idle queues
A runner pool's queue statistics could show "NaN" for the message counts of a newly created or idle queue — one that hadn't yet handled any traffic — and the message handling rate mirrored the publish rate instead of showing its own value. Queue counts and rates now read 0 until there is activity, and the handling rate reflects actual message delivery. (Core 1.236.2, Core-Api.)
Dashboard: automatically generated avatars have a new look
The placeholder avatars shown for items without a picture — people, vendors, projects, teams, apps and more — have been redesigned. Each avatar now shows the item's initials over a colourful pattern generated from its name, so the same name always produces the same avatar and different items are easier to tell apart at a glance. (Web App 2.34.0.)
Dashboard: personal access tokens support longer expirations, and editing a token no longer changes its scope or expiry
Personal access tokens can now be created with 1-year and 2-year expirations, alongside the existing 7-, 30-, 60- and 90-day options — useful for long-lived integrations (such as a Magento connection) that shouldn't need re-issuing every quarter. A token's expiration is set when it is created and stays fixed for its lifetime; to change it you create a new token. Editing an existing token now updates only its name and no longer silently resets its scopes or IP allow-list. (Web App 2.34.0, @attlaz/client 1.83.0, Core-Api.)
Logging: database warnings and errors are logged as readable text again
Warnings and errors from the database layer — most visibly a connection or query timeout — were being written to the log one character per line instead of as a single readable message, because the message text was mistaken for structured log data. These messages now log as a single readable line again, with any structured details kept in the log context where they belong. (Core TBD.)
Dashboard: product apps show the correct icon and name everywhere
Each product app's icon and name are now taken from a single source, so they display consistently in the sidebar, the applications list, and the app's setup screen. Previously Enhance Catalog reused another app's icon, and a couple of setup screens showed a mismatched app name (for example, Enhance Catalog's intro was labelled "eCommerce Monitoring"). Enhance Catalog now has its own icon, and every app's name and icon match across the dashboard. (Web App 2.34.0.)
2026-07-21
PHP: base packages are now public and a project skeleton is available
The core PHP building blocks — attlaz/project-base and attlaz/adapter-base — are now published on public Packagist, so a PHP project resolves them and their dependencies (attlaz/client, attlaz/attlaz-monolog, echron/tools, echron/datatypes, echron/io) from Packagist without private-repository credentials. A new composer create-project attlaz/skeleton . scaffolds a ready-to-run project already wired to attlaz/project-base. New PHP projects target PHP 8.4+. See the PHP platform reference.
2026-07-16
API: access-denied and not-found now return the correct status code
Some endpoints returned a generic 500 Internal Server Error when the caller lacked access to a resource, or when a resource didn't exist — most visibly when requesting an adapter connection's capabilities or executing a connector operation the caller has no access to. These now return the correct status platform-wide: 403 Forbidden for an access denial and 404 Not Found for a missing resource. (api-lib 1.58.0.)
2026-07-11
Adapters: viewing a connection's capabilities now requires access to its project
Requesting the list of operations a connection supports (its capabilities) did not verify that the caller had access to the connection — any authenticated user could read the capabilities of any connection by its id. It now enforces the same project-access check used elsewhere, so a connection's capabilities are only visible to users with access to the project that owns it. (Core 1.231.0, Services-Api.)
Deployments: build commands are served centrally and builds use the platform's active image
Building a code source (installing dependencies, compiling) previously ran a build command hardcoded in the deploy service and used the newest available platform image. The build command now comes from the code source's build strategy, served from the API as the single source of truth — the same model the run strategy moved to on 2026-07-09 — so adding or changing a build strategy no longer needs a deploy-service release. Builds and flow runs now both use the platform's active image (the vetted one), so a newer platform image that is still being tested is not used until it is promoted to active. (Core 1.229.0, Deploy-Service, Worker.)
JavaScript SDK: the client keeps its access token when client credentials are also configured
When a @attlaz/client was created with an access token and then also given client credentials (used to renew the token when it expires), the token was being discarded — so the very first API call failed with "access token not provided". This could surface as an Attlaz Node project's flow run failing to authenticate. The client now keeps the token, and when no valid token is present it authenticates on demand using the client credentials, matching the PHP client's behavior. (@attlaz/client 1.82.0.)
2026-07-10
Runners: more reliable and resilient flow-run execution
A batch of reliability improvements to how runners execute flows. Previously, a small number of edge cases could disrupt flow processing on a runner:
- An unprocessable ("poison") message on the queue could repeatedly crash a runner as it was redelivered; such a message is now dropped safely instead of looping.
- If a runner briefly lost its connection to the message broker while a flow was running, it could come back but stop picking up new work (a silent "zombie"); runners now reliably resume consuming after a reconnect.
- A flow run's result could occasionally come back empty when the runner's reading of the flow's output was split at an unlucky boundary; the output is now reassembled correctly.
- A transient infrastructure hiccup while a run was starting up (for example loading the deploy) previously failed the run outright; these are now retried briefly before giving up, so a momentary blip no longer fails an otherwise-healthy run.
- Deploy artifacts are now unpacked atomically, so a runner interrupted mid-unpack can never leave a partial deploy that a later run might pick up.
One visible behavior change: a flow run now stays Pending until it actually begins executing, rather than flipping to Running the moment a runner picks it up (before its code is even downloaded). (Worker 1.26.0, Core 1.228.0.)
Runners: flows always run within a memory and CPU limit
A flow with no explicitly configured resource limit previously ran without a memory or CPU cap, so a single runaway flow could consume a whole runner node and affect other flows scheduled there. Flow runs now always run within a limit — the configured one when set, otherwise the runner node's safe maximum — so one flow can no longer starve the others. (Worker 1.26.0.)
2026-07-09
Deployments: changing a code source's run strategy takes effect without a runner restart
Changing the run (or build) strategy on a code source didn't reliably take effect: a runner is a long-lived process that had cached the old strategy in memory, so it kept executing flows with the previous run command until the runner happened to restart — which could surface as a flow failing to start (e.g. looking for a dist/index.js entry point that the newer strategy doesn't use). Runners now resolve strategies centrally and refresh them in the background, so a changed strategy is applied on the next run within a short window, with no restart needed. Strategy definitions are also served from one place now instead of each service carrying its own copy. (Core 1.226.0.)
2026-07-07
JavaScript SDK: the client package moved to @attlaz/client
The Attlaz JavaScript/TypeScript client, previously published to npm as attlaz-client, is now published under the Attlaz scope as @attlaz/client. The API is unchanged and the version line continues (the last attlaz-client was 1.76.x; @attlaz/client picks up at 1.77.x). The old attlaz-client package is deprecated — its published versions stay installable so existing projects keep working, but it will no longer receive updates. To move over, change the dependency and imports from attlaz-client to @attlaz/client (subpath imports such as attlaz-client/Model/... become @attlaz/client/Model/...). The dashboard and all Attlaz services have already switched. (@attlaz/client 1.77.1.)
2026-07-03
Deployments: requesting a code deploy works again
Requesting a code source deploy from the dashboard failed with a generic error, and the deploy was then left stuck on Pending without ever running. Two issues combined. First, an internal wiring bug meant the step that refreshes a code source's Git credential — needed because short-lived OAuth tokens (e.g. GitLab, ~2h) would otherwise fail the repository clone — was set up without its network client and threw. Second, when the failed request tried to mark the deploy as Failed, a separate bug on freshly created deploy records blocked the status update, so the record stayed Pending instead of surfacing the failure. Both are fixed: deploys are requested and run normally again, and a deploy that genuinely fails is now correctly marked Failed rather than hanging on Pending. (Core 1.225.1, Core-Api.)
2026-07-02
Logging: secrets are no longer stored in log entries
Values under sensitive keys — passwords, tokens, API keys, authorization headers, and similar secrets — were masked in the live console output but were still written in clear text to stored logs (and to alert messages). They are now masked everywhere a log entry travels: the stored log and any alert. Secrets no longer linger in log storage. (Core 1.224.0.)
Logging: a problematic log context can no longer disrupt the operation being logged
Building a log entry from an unusual context value could, in rare cases, throw and interrupt the very operation that was writing the log. Log construction is now fully isolated — a problematic value is safely summarised instead of failing — so writing a log can never break application flow. Two robustness improvements ship alongside: stored logs are now written in batches (fewer round-trips to the log database), and the pending-log buffer is bounded, shedding the oldest entries if the log database is briefly unavailable rather than growing without limit. (Core 1.224.0.)
2026-06-27
Dashboard: consistent menu and dropdown styling
Menus and dropdowns across the dashboard had drifted into several different looks — action menus, the environment switcher, the project/workspace switcher, autocompletes, and the filter value picker each had their own surface, spacing, and hover style. They now share one consistent style: a single panel surface (corner radius and shadow) and Windows 11-style items rendered as individual rounded "pills" that sit slightly inset, with a soft fade on hover instead of a full-width highlight. Item padding is uniform whether or not an item has an icon. Purely a visual change — no change to what any menu does. (Web App.)
2026-06-26
Dashboard: the "Add flow" button works again
Clicking + Add flow on a project's flows page did nothing. The button navigated using an old-style URL that the app's legacy→current redirect collapsed back to the flow list — silently dropping the segment that opens the new-flow page. It now goes straight to the create-flow page. (Web App 2.31.1.)
Dashboard: an expired session now sends you to sign in instead of hanging
Opening the app with a saved session whose login had expired (the refresh token was no longer valid) left the app stuck on the loading screen indefinitely — it never showed the sign-in page. The expired-session response from the token endpoint wasn't recognised as an authentication failure, so the app treated it as a generic error and, because of a second issue, couldn't render the error page either — leaving just the Attlaz logo on a blank screen.
An expired or rejected session is now correctly treated as signed-out: the app clears the session and redirects to the login page. Related fixes shipped alongside:
- Error pages render properly. A genuine error (e.g. a backend outage at startup) now shows the "something went wrong" page instead of a blank screen with the logo.
- Failed sign-ins show the real reason. A rejected login now surfaces the server's actual message (e.g. invalid credentials) rather than a generic HTTP status.
- Unexpected API response shapes degrade gracefully. If a single record in a list can't be read, the rest of the view still loads instead of the whole page failing, and such cases are reported for monitoring instead of being silently mislabeled as "not found." (attlaz-client 1.76.0, Web App 2.31.1.)
2026-06-24
Storage: listing bucket items no longer fails past the first page
Listing the items in a storage bucket (e.g. a project environment's persistent storage) returned a 500 Internal Server Error — "Unable to get storage item keys" — as soon as the bucket held more items than fit on one page and a second page was requested. The pagination cursor the API hands out is the item's id, but the storage backend was looking that cursor up by item key instead of by id, so the lookup always missed and the request errored. Buckets that fit on a single page were unaffected, which is why this only surfaced as buckets grew. Item listing now pages correctly through all items, and a deleted or expired item at the cursor position no longer breaks paging. A malformed cursor now returns a clear 400 Bad Request instead of a 500. (Core 1.221.0.)
APIs: clearer errors and higher limits for large request bodies
Sending a request body larger than an API allowed produced a confusing 500 Internal Server Error with an empty error code ({"error":{"code":"","message":"request entity too large"}}), and the rejection wasn't clearly logged — there was no easy way to tell which API rejected the request, what the limit was, or how big the body actually was. This surfaced when a Google Sheets setSheetValue call carrying a large payload failed against the adapter API.
Three changes address it:
- Oversize bodies now return a proper
413(Payload Too Large) with apayload_too_largeerror code, instead of a generic 500. The rejection is logged as a warning that names the route, the configured limit, and the actual body size — so it's clear which service rejected it and why. (api-lib 1.55.0.) - The adapter (Services) API body limit was raised from 100 KB to match the core API, so legitimately large adapter payloads (e.g. writing many rows to a sheet) are no longer rejected. (Services-Api.)
- The API Gateway now enforces a per-route body size limit at the edge, rejecting an oversize request with a
413before it's forwarded to a backend — a faster, clearer failure. Current ceilings: 10 MB for adapter/services calls, 2048 MB for storage uploads, and 1 MB for everything else. Requests are checked by their declared size; the backend limit remains a final safety net. (Gateway-Api.)
Chunking large payloads client-side is still recommended where practical.
2026-06-12
Dashboard: runner pool memory & CPU limits now display correctly
The runner pool settings screens were under-reporting resource limits in three ways, now fixed:
- The runner pool overview always showed "-" for both Memory and CPU on every pool, even when the deployed runners had limits set. The live deployment reader was looking at the wrong place in the Kubernetes object, so it never found the limits; it now reads them from the container. (Infrastructure service.)
- The edit runner pool dialog showed an empty Memory (or CPU) field whenever the configured value wasn't one of the preset quick-pick options (e.g. an 18 GB memory limit). Any configured value now appears in the dropdown even if it's not a preset. (Web App.)
- The overview's CPU limit was formatted as if it were a data size; it now reads as cores / millicores (e.g. "2 cores", "1500m"). (Web App.)
No change to how limits are configured or applied — these were display-only fixes.
Dashboard: filters redesigned and made responsive
The table filters in the dashboard got a usability and mobile overhaul. Each filter chip now shows a dropdown affordance (a down-chevron) so it's clear it opens a picker, active filters are visually distinguished (a light-grey fill), and long values truncate instead of overflowing. The configuration popover now repositions itself to stay on screen instead of running off the edge. On phones the whole filter bar collapses to a single Filters button that opens a centered dialog listing every filter, with a Done button to close. (Web App 2.30.0.)
Worker: hardened flow-run container stats parsing
The Worker's per-flow-run resource-stats reader threw a TypeError ("Cannot read properties of undefined (reading 'cpu_usage')") — and logged it repeatedly — whenever Docker returned an empty or partial stats sample for a container that had just started or was stopping; the run was also briefly mis-reported as not running in that window. Stats parsing now treats a missing CPU/memory sample as zero (the container stays reported as running). No behavior change when stats are present. (Worker.)
Dashboard: more reliable startup — navigation, loading and sign-out
Several startup issues were fixed:
- The main navigation could render with permission-gated items missing and need a refresh (or several) before they appeared — a race between environment loading and permission loading. The menu is now built reactively from both, so it shows the correct items the first time.
- A failed or slow startup (offline API, a transient error) no longer leaves the app stuck on an endless loading spinner, and a network hiccup no longer silently signs you out. Failures now show an error/offline page, and only a genuine authentication failure signs you out.
- The app now waits for permissions before revealing the dashboard, removing a brief flash of an incomplete menu on load.
No change when things load normally. (Web App 2.30.0.)
Project and workspace lists now load every page
Project and workspace lists were only loading the first page (up to 100 items), so a workspace with many projects — or a user belonging to many workspaces — could have entries silently missing from switchers and lists. Both now follow the cursor through all pages. (attlaz-client 1.72.0, Web App 2.30.0.)
2026-06-11
Platform reliability: Kubernetes health probes wired across all services
Every platform workload — APIs, background/queue services, and flow-run runners — is now monitored via Kubernetes liveness, readiness, and startup probes against the built-in health server. Effects:
- A process whose event loop is dead or wedged is now automatically restarted (liveness) instead of lingering broken. This closes a failure mode where a runner could stop consuming flow runs after an upstream outage and stay stuck until someone restarted it manually.
- A pod only receives traffic / counts as rolled out once it has started, has healthy dependencies, and is actually listening (readiness) — reducing brief 5xx windows during deploys.
- Readiness comes from a reworked startup contract in the shared runtime (
@attlaz/attlaz-coreAppRunner): a service reports ready based on dependency health and serving state, and is no longer held back by long-running initial work (e.g. a maintenance pass). Probes are durable — runner deployments get them from the infrastructure service that creates them; all other workloads from their workload definitions.
Gateway-Api: single point of failure removed + route loading hardened
The API gateway now runs with multiple replicas (was a single pod). Route loading was hardened: the gateway fails fast on startup if it cannot load its routing table (instead of coming up empty and returning "no route found" 500s for every request), and a failed periodic route refresh now logs and keeps the last-known-good routes rather than crashing the process. Combined with the readiness probe above, a gateway pod stays out of rotation until its routes are loaded.
2026-06-10
Flow runs no longer flip from Canceled to Failed; run-duration overflow fixed
A pending flow run that had been cancelled (e.g. by the 7-day stale-run cleanup) could later be picked up by a runner draining a stale queue message, get wrongly marked Failed, and have its run duration computed across the whole span — overflowing the duration column and logging "Run duration bigger than max unsigned int(11)". Root cause: the runner's "has this run already finished?" check trusted the eventually-consistent index projection (which could still read Pending), while the indexer synthesized a missing started event and let a stray late terminal overwrite the run's end time. Fixes:
- A flow run's status is now derived from its event history with first-terminal-wins semantics — the first terminal event (canceled/complete/failed/stopped) is final, later contradictory events are ignored, and a
startedis never fabricated. (@attlaz/attlaz-core, Command-Service.) - Runners now consult an authoritative status endpoint (
GET /flowruns/:flowRunId/lifecycle, derived from history) before executing, so they reliably skip already-cancelled/finished runs instead of re-running and failing them. (Core-Api, Worker.) - The JS client gained a
FlowRunLifecyclemodel +getFlowRunLifecycle(), plus a dedicatedFlowRunHistoryStatusso history rows parse correctly. (attlaz-client JS.) - One-time data repair: 2,280,408 historical
flow_run_historyrows (fabricated starts and contradictory post-cancel terminals, almost all 2017–2019) were removed so those runs report their correct Canceled status; the removed rows were backed up first.
Connections page no longer hangs; adapter categories load correctly (bug fix)
Opening Your connections could leave the table spinning forever with no error, and the Adapter catalog could show "Empty" alongside an "Unable to load adapters" error. Both traced to the same backend defect: the GET /adapter-categories endpoint ignored pagination and failed on every call (HTTP 500), and the connections page loaded categories outside the table's data path so the failure never surfaced. The endpoint now paginates correctly, and the dashboard routes the failure into the table's error state instead of an endless spinner. (@attlaz/attlaz-core 1.216.0, Core-Api, Web App 2.28.1.)
Platform hardening: cursor pagination is now required at compile time
The shared data layer's cursored-read primitive (fetchCursoredResults) now requires a pagination argument in its signature, rather than checking for one at runtime and throwing a 500 when it was forgotten. Because every paginated list query in the platform flows through this one function, the requirement is enforced for all of them at build time — a missing pagination is now a compile error, not a production incident. Wiring this up surfaced seven other list endpoints that were silently failing or ignoring pagination the same way (content-generation batches/strategies/proposals, catalog feeds, flow-run relations, and product-match jobs); all were fixed in the same pass. (@attlaz/attlaz-core 1.216.0, @attlaz/apps-lib 1.47.0, Core-Api.)
Dashboard: consistent error states across tables, widgets and dialogs
Several dashboard views previously swallowed load failures — leaving a stuck loading skeleton, a misleading empty state, or nothing at all. They now show a clear error (and, for tables, a toast) when data can't be loaded:
- Tables that pre-load supporting data (storage bucket items, flow runs) no longer hang if that supporting load fails — the error appears in the table instead.
- Competitor-monitoring widgets (account summary, price segments, profit segments) show an error state instead of silently rendering blank.
- The storage item details dialog no longer spins forever if the item fails to load.
No behavior change when things load normally. (Web App 2.28.1.)
2026-06-09
Maintenance: retired a redundant log-stream validation pass
The ValidateLogStreams maintenance step was removed from the cycle (kept, deprecated). It failed every run at current scale — its "all distinct log streams in one document" aggregation exceeded MongoDB's 16 MB document limit — and a read-only sample found no orphaned log streams in practice (flow-run logs are already purged on the 14-day retention by the dedicated purge step). Removing it stops a recurring error and reclaims time each cycle, with no behavior change. (Maintenance service.)
Storage maintenance: expiry pass no longer scans large non-cache buckets
Expired-item cleanup now only inspects cache buckets (the only tier whose items expire); persistent/vault/infrastructure buckets — which can be multi-GB — are skipped rather than scanned each cycle. The TTL index still auto-expires any stray item that carries an expiration. (@attlaz/attlaz-core 1.215.3.)
Flow run history: filter by status
The flow run history table in the dashboard can now be filtered by run status (Pending, Running, Complete, Failed, Canceled, Stopped) — pick one or more from the Status filter to narrow the list. Filtering is applied server-side and works correctly across pages. Previously the GET /flows/:flowId/runsummaries endpoint accepted no status filter (the repository supported it, but the controller dropped the query parameter), so the list always showed every run. (Core-Api, Web App.)
Web App (dashboard): upgraded to Angular 22 + TypeScript 6
The management dashboard was upgraded from Angular 21 to Angular 22 (with TypeScript 6). This is an internal modernization with no user-visible behavior change — existing dashboard functionality is unchanged. Highlights:
- The router's
paramsInheritanceStrategywas explicitly pinned toemptyOnlyto preserve pre-22 route-parameter behavior (Angular 22 changed the default). - Component outputs were migrated to the signal-based
output()API, and the app remains fully zoneless and standalone (244/244 OnPush components). - Build tooling and lint/test dependencies were refreshed (TypeScript 6, jsdom, eslint-plugin-jsdoc); ESLint is held at 9 pending plugin support for 10.
- Web App
2.28.0, deployed.
GET /projects now paginates correctly (bug fix)
The project list endpoint now returns a properly cursor-paginated set of the projects you can access (via active workspace membership). Previously it paginated workspaces internally and always reported has_more: false, so a user in more than ~10 workspaces silently lost the projects in the excess workspaces. It now runs a single membership-scoped query over projects with a real project cursor. (@attlaz/attlaz-core.)
Clients: list endpoints are uniformly cursor-paginated
The JS and PHP clients now return a paginated CollectionResult (with has_more) from every list method and accept a CursorPagination — flows, flow-run summaries, connections, adapter/connection configuration, projects, project environments, config values, log streams, and storage bucket items. Both clients gained a LoadAllHelper to walk an entire collection when you need every record. Previously the PHP client returned only the server's first page (default 10) and silently dropped the rest — most impactfully, a project with more than 10 connections or config values would have its connection pool truncated at runtime. (attlaz/client PHP, attlaz-client JS.)
2026-06-08
Storage: scheduled maintenance (expiry + orphan cleanup)
The Maintenance service now actively maintains storage on its schedule (previously a no-op stub):
- Expired items are removed. Simple items past their
expirationauto-expire via a MongoDB TTL index; the maintenance pass also sweeps any stragglers (a safety net) and deletes expired large (chunked) items together with their GridFS chunks. - Empty, stale buckets are garbage-collected. A bucket whose collection is empty and untouched for 7+ days has its row removed (and its empty collection dropped). Buckets are re-created on demand, so this is safe. (
@attlaz/attlaz-core, Maintenance service.)
Platform fix: DELETE/UPDATE with a row limit on PostgreSQL
The shared data layer appended LIMIT 1 to single-row DELETE/UPDATE statements (a MySQL safety). PostgreSQL rejects LIMIT on DELETE/UPDATE ("limit has no effect"), so any such write against a Postgres-backed table failed to compile. The limit is now applied on MySQL only. This was latent until the new storage maintenance issued the first-ever delete against the Postgres storage_bucket table. (@attlaz/attlaz-core 1.215.2.)
Supervisor (health monitoring): stability fixes
The service that runs per-environment health checks no longer restart-loops under load.
- Overlapping health sweeps are prevented. A sweep that runs longer than its 5-minute interval no longer starts a second concurrent sweep on top of itself (which previously deadlocked on the health-alert table and could crash the service). Late ticks are skipped while a sweep is still running.
- A failing check can no longer crash the service. Health-check failures are caught and logged instead of taking the process down.
- Transient database deadlocks are retried. Writes to the health-alert table now retry briefly on a deadlock (MySQL/MariaDB
1213/ Postgres40P01) before surfacing an error — handled generically in@attlaz/attlaz-coreso any repository can opt in.
Storage: unique-key index now guaranteed at write time (correctness)
A storage bucket's unique item-key index is now created when the bucket is first written to. Previously it was created only by the periodic health sweep — and only for Cache/Persistent tiers — so a freshly-created bucket (and any Vault/Infrastructure bucket) could go without its unique-key index until a sweep ran, leaving a window where duplicate keys could be written. All four tiers are now indexed on first write. (@attlaz/attlaz-core.)
Storage: health check is read-only + faster storage info
- The storage health check no longer performs index maintenance (that moved to the write path), so it's now a pure read.
- Computing a tier's usage now queries each bucket's collection stats with bounded concurrency instead of sequentially, speeding up the storage-info endpoint, the infrastructure-status updater, and the health check on environments with many buckets. (
@attlaz/attlaz-core.)
Reliability: large result sets no longer silently truncated
Several internal listings that fetched a fixed-size page and treated it as "everything" (project lists, scheduled triggers, log-stream and stale-flow-run cleanup, search reindex, expired-token reporting, and others) now page through the full set (or stream it). Previously, once a collection grew past the hard-coded page size, the extra rows were silently dropped — e.g. scheduled triggers beyond the cap would stop firing, and cleanup jobs would skip the overflow. (@attlaz/attlaz-core and infrastructure services.)
2026-06-07
Storage: getItemKeys now returns every key (bug fix)
Listing a storage bucket's keys now returns the complete set of key strings. Previously the PHP client's getItemKeys was annotated string[] but returned the first page of item records (arrays of {key, …}), so a caller doing foreach (getItemKeys($pool) as $key) got arrays instead of strings — and only the first 1000 items. The convenience now lives in StorageEngine::getItemKeys() (PHP project base), which paginates through every page of the items-information listing and projects each record to its key. (attlaz/client PHP.)
Storage: item size accounting fix + faster listing
- Chunked (large) items now report their true size. They were being overwritten with the size of their (value-less) metadata document, underreporting large items to a few hundred bytes.
- Bucket item listings are materially cheaper on large buckets. The listing no longer recomputes each document's BSON size on read (the size is already stored at write time, and the recompute forced MongoDB to read every full document — including large values). (
@attlaz/attlaz-core.)
Storage: "pool" renamed to "bucket" (terminology unification + deprecations)
The storage grouping is now consistently called a bucket across the API, both clients, and the Web App (it was already StorageBucket internally; "pool" lingered in method/parameter names — and "pool" is reserved for runner pools).
- Client methods renamed:
getPoolItemsInformation→getBucketItemsInformation,getPoolKeys→getBucketKeys,clearPool→clearBucket(JS + PHP clients, and theStorageEnginePHP wrapper). The old names remain as@deprecatedaliases so existing flow/integration code keeps working; they will be removed in a future release. - Storage-information endpoint (
GET /projectenvironments/:id/storage/:type) now returns the bucket list under a newbucketsfield. The legacypoolsfield is still emitted with identical contents for backward compatibility and is deprecated — new clients should readbuckets. The clients readbucketsand fall back topools, so they work against both old and new servers. - No URL changes (the bucket key was always a path variable, not the literal word "pool").
Requester / user display names (admin views)
Admin lists that reference a user id now resolve it to a display name through a shared, access-controlled resolver (@attlaz/attlaz-core UserSummaryService).
- The Command executions "Requested by" column and the Events (audit log) "User" column now show names instead of raw ids. The platform system user shows as "System".
- Visibility is access-controlled: you only see the names of users you share a workspace with (admins see all). Users you can't see, or that no longer exist, fall back to a neutral label. Inactive/removed users are labelled accordingly (e.g. "… (inactive)", "Removed user"). No email or other PII is ever exposed by this resolver.
Events (audit log) admin view: fixes + filtering
- IP addresses render correctly. The API now serializes an IP address as a plain string everywhere it appears (audit log, OAuth tokens'
ipand IP allow-list), instead of a nested object that showed as[object Object]. (@attlaz/api-lib.) - Action column now shows the action verb when an entry has no description (it was blank before).
- Resilient to bad rows. A single audit entry whose entity reference can't be resolved (unknown/removed type, or none) no longer blanks the whole page — the reference is logged and shown as
unknown, and the rest of the page renders. - New filters: by user (multi-select of the users that appear, by name), by entity (a prefixed entity id), and by date range.
Command executions admin view: filter by requester (by name)
The "Requested by" filter is now a multi-select of the actual requesters, shown by name (previously a free-text field that only matched a raw user id, which was effectively unusable).
Web App: reusable date-range component (<app-date-range>)
A new reusable date-range picker (built on Angular Material) powers the events date filter. Its public API is picker-agnostic, so the internals can be upgraded later (e.g. to a presets-plus-calendar selector) without touching the pages that use it. Dates display in the user's configured profile date format (consistent with the rest of the app), and the open calendar shows a dim backdrop.
Library/Core: FetchRows supports same-column range filters (@attlaz/attlaz-core 1.211.x)
Two filters on the same column (e.g. time >= from AND time <= to) now work. Previously they collided on the bound-parameter name and the query silently matched almost nothing — a latent bug for any range filter, surfaced by the new events date filter. Each filter now gets a unique placeholder.
2026-06-06
Web App: command-history refresh + pinned log filters
- Command executions list gained a "Refresh view" header action (re-runs the current page via the table's
CollectionDataSource.refresh()). - Log viewer filters: Log level is now always shown as a chip (instead of hiding behind "+ Filter"), and on the system log the app tag filter is shown by default too — using the
<app-filters>alwaysVisibleoption added the previous day.
2026-06-05
Command execution history + admin view
Background platform commands (clear log stream, reindex flow/flow-run/search, generate flow-run report, flush pending flow runs, …) are now tracked end-to-end and surfaced in an admin view. (@attlaz/attlaz-core 1.204.0.)
- Lifecycle tracking. A command now records a
pendingrow when it is enqueued, then transitionsrunning→completed/failedwith start/finish timing and a structured JSON result (e.g. clearing a log stream records{ "removedCount": N }). Failures store the real error message instead of silently succeeding. - "Command executions" admin view (System → Admin) lists commands with status/stage, who requested them, duration and result, plus a detail view showing parameters, result and error. Admin-only.
- Who requested what. User-initiated commands (e.g. an admin clearing a log stream) also write an audit-log (
user_action) entry; cron/event/maintenance-triggered commands are attributed to the platform system user and are not audited. - Reliable log-stream clearing. Clearing a log stream now runs fully in the background and deletes in bounded batches, so large streams no longer time out. It clears entries up to the moment of the request (logs written during a long clear are preserved), reports how many entries were removed, and surfaces real failures.
- Command ids now carry a
crq_prefix (internal/admin only). - All commands are enqueued through a single
CommandDispatcher(which writes thependingrow); the rawCommandService.postToQueueis no longer used directly by producers. - Adds columns to
system_command_execution(result,requested_by, nullablestart/finish,data/errorwidened toTEXT) plus status/type/requested_by indexes.
Web App: filter component enhancements (<app-filters>)
- Always-visible filters. A filter can be pinned so it always renders as a (possibly empty) chip instead of hiding behind "+ Filter". Used for the command-history Status/Command filters and the log viewer's Log level (and the
apptag on the system log). - Display label vs underlying value. Filter options can carry an underlying value distinct from the shown label, so filters display human-readable labels (e.g. "Clear log stream") while matching on the code (
clear_log_stream). The command-history Status/Command filters support selecting multiple values.
Web App: "Try again" button on table load failures
When a table fails to load its data, the in-table error state (<app-error>) now shows a Try again button that re-runs the load directly — no full page reload. Because every <app-table> is backed by a CollectionDataSource (which exposes refresh()), this works for all table call sites automatically (connections, collections, events, etc.). The original error title/message is still shown above the button.
ErrorComponentgained a single optionalretryinput: a(() => void) | nullhandler (defaultnull). When set, the button renders and invokes it; when unset, no button shows. The presence of the handler is the "this error is recoverable" signal — there's no separate boolean flag.TableComponentpasses a stable bound handler that callsdataSource.refresh(). On retry the table swaps the error view for its loading skeletons (the datasource'sloading()flips true), so the retry is visually obvious.- Other
<app-error>consumers that don't setretry(e.g.log-list) are unchanged.
2026-06-03
Library/Core: FetchRows query-builder hardened (@attlaz/attlaz-core 1.202.0)
Repositories now have a structured way to express filters that previously forced hand-written cursor SQL — OR groups, IS NULL on a function result, NOW()-against-a-column, cross-column comparisons. The first migration uses it: OAuthAccessTokenSqlRepository.getUserAccessTokens had a separate getUserAccessTokensByStatus manual-cursor method specifically because FetchRows couldn't express expires IS NULL OR expires > NOW(); that method is deleted and the status filter folds into the FetchRows path via addFilterRaw. Same end-to-end behavior, ~70 lines removed.
FetchRows.addFilterRaw(sql: string)— splice a literal SQL fragment into the WHERE chain, ANDed with structured filters. No bound parameters in the fragment (useaddFilterFieldfor those, to avoid placeholder name collisions). Caller owns dialect portability of the fragment. Repository-only — never accept user input.FetchRows.addLeftJoin/addInnerJoinpush to a joins array instead of overwriting — multiple joins now supported and emitted in declaration order.FetchRows.addField(field, type, alias?)— optionalaliasparameter emitsfield AS aliasin the SELECT clause.addFilterFieldtype-mismatch validation now throws instead ofconsole.error-ing. Same for the previously-silent failure of declaring a field twice with differentFieldTypes.IS NOT NULLtypo fixed —addFilterField(field, null, ..., NotEqual)previously emittedNOT IS NULL. Latent bug, no production call site used it.- Empty
Infilter collapses to1 = 0(mathematically correct: "WHERE x IN ()" returns nothing). EmptyNotInthrows ataddFilterFieldtime — silently expanding to "match everything" is usually a security/correctness bug (e.g.addFilterField('user_id', authorizedIds, NotIn)with an empty allowlist). FieldType.Ip/FieldType.Uuidthrow on Postgres with a clear message — the current Attlaz Postgres schema stores these asbytea/text, not the native column types, so the previous silent passthrough would have produced wrong results if anyone used them. MariaDB behaviour is unchanged.- Postgres identifier escaping now doubles embedded
"per the SQL standard. Repositories control field names today so practically a defense-in-depth fix. - EndingBefore pagination no longer wraps in a subquery. The helper emits
WHERE id > ? ORDER BY ASC LIMIT N+1;AbstractRemoteService.fetchCursoredResultsreverses in JS. Eliminates the latent MySQL duplicate-column issue when EndingBefore is combined with JOINs. fetchCursoredResultsthrows ifsetPaginationwasn't called (was a silent warn + default 100). Catches the omission at the right boundary; no production call site relied on the silent default.FieldType.KSUIDenum value removed — never used; its handling was a duplicate ofBINARY.- Internal-only renames in
FetchRowsQueryHelper:createHmmz→castColumnForRead,createPlaceHolder→castValueForBind. Private methods, no external callers.
Sort-by-non-identifier still requires a manual cursor query (ATL-271 tracks the composite-cursor follow-up that brings sort under FetchRows too). Until then, the FlowSummary pattern is still the reference.
Security: all password writes now argon2id, with upgrade-on-login (@attlaz/api-lib 1.52.2, deployed via Core-Api)
Password hashing is now consistently argon2id across every write path, with transparent migration of legacy hashes. Part of the ATL-268 sensitive-data audit. Specifically:
- Upgrade-on-login. Successful password logins re-hash the stored password with argon2id when the existing hash is outdated (legacy bcrypt, or argon2 with weaker-than-current parameters) and persist it — closing a previously-unimplemented
// TODO: update password hash if neededin the OAuth password-grant (OAuth.ts). Best-effort: a failed re-hash is logged and never blocks an otherwise-valid login. - Change-password and password-reset now write argon2id. Both flows previously called bcrypt directly (an unfinished migration — registration was already argon2). They now route through the
UserHelper.setUserPasswordwrapper, which hashes with argon2id. Controllers no longer call the hashing implementations directly. - argon2id parameters pinned (
memoryCost 64 MiB, timeCost 3, parallelism 4) instead of relying on library defaults, so a futureargon2upgrade can't silently change the cost. Values match the existing stored-hash format (no forced rehash) and exceed the OWASP argon2id minimum.
Net effect: bcrypt is now only ever read (to verify pre-existing hashes) and drains out of the database as users sign in. No user is locked out — existing bcrypt hashes still verify.
2026-06-02
Code source account details, API key status + filtering, code source tooltip platform, "Manage" shortcut
A batch of Web App consistency/UX improvements plus the server-side work to back them.
-
Code source account details now match the adapter-connection layout. The detail view (
edit-source-account-page) previously renderedName/Stateas plain<div>text. It now uses read-onlyapp-form-fieldinputs — Name, Source account ID (the internal id, previously not shown), State — mirroring the adapter-connection configuration page. Also fixed a latent bug where the form patchedname/stateto empty strings instead of the loaded values. -
"Manage" button on the code source page. The "Code" block on the deployment/code-source page (
deployment-page) now has a Manage button linking straight to the owning code source account's details (EntityNavigationService.codeSourceAccountsDetails). No extra fetch — the account is already loaded. -
Code source tooltip shows the runtime platform. The tooltip used when viewing a flow (
CodeSourceTooltipComponent) now displays the runtime platform name (e.g. "Node 20.9") under the repository details. The provider (github/gitlab) is already conveyed by the icon, so the runtime platform is the useful addition. Resolved viaPlatformService.getPlatformById, which reads the cached platform list — no per-hover or extra network fetch (resolved once when the tooltip data is built on flow-overview load). Newplatform: string | nullfield onCodeSourceWithData. -
API keys: expired keys are kept, shown, and filterable.
- Retention fix (Maintenance-Service).
OAuthTokenMaintenance.purgeOauthTokens()was hard-deleting user API keys 30 days after expiry, lumping them in with ephemeral OAuth artifacts (authorization codes, refresh tokens). User API keys (type='user') are now excluded from the purge — they persist until the user deletes/revokes them. The other two token tables still purge as before. - Status column (Web App). The API keys table gained a Status column with a derived badge: Active / Inactive / Expired / Revoked. "Expired" is computed from
expires(it is not a stored state — a key keeps its Active/Inactive state past expiry). - Server-side status filter. New
UserAccessTokenStatusenum (active/inactive/expired/revoked) in Library/Core (mirrored inattlaz-client).GET /access-tokensaccepts?status=(parsed viaRequestHelper.getEnumFilter).OAuthAccessTokenSqlRepository.getUserAccessTokensgained an optionalstatusparam: the no-filter path is unchanged (still returns expired keys, since it filters only on state); the filtered path uses a hand-written cursor query (expired⇒expires <= NOW(),active/inactive⇒expires IS NULL OR expires > NOW()) becauseFetchRowscan't express the OR-over-nullable condition. Revoked keys appear only when explicitly filtered. The Web App wires this through<app-filters>(single-select; the backend takes one status at a time).
Deployment order: publish Core → rebuild/deploy Core-Api; publish
attlaz-client1.64.0 →npm install+ build/deploy Web App; build/deploy Maintenance-Service (self-contained). - Retention fix (Maintenance-Service).
2026-05-30
Filter UX follow-ups: CDK Overlay migration, anchored "+ Filter", Enter to apply, fixed global keydown swallower
Continuation of the 2026-05-29 filter-tree work. The filter options popover (FilterValueSelectDialogComponent) moved off MatDialog onto a declarative CdkConnectedOverlay, matching the AutocompleteComponent pattern that was already established in the project. The pending "+ Filter" anchor bug, the Apply-on-Enter UX, and a project-wide global-keydown bug were all fixed in the same pass.
-
MatDialog→ declarativeCdkConnectedOverlayfor the filter options popover.FilterValueSelectDialogComponentis no longer Dialog-coupled:filteris aninput.required<Filter>(), the Apply action is an@Output() appliedemitter, and the component owns its own surface (white background, 1px gray-300 border, 4px radius, soft shadow) via:hostSCSS rather than relying onMatDialogelevation.FiltersComponentholds the overlay with<ng-template cdkConnectedOverlay>and signals (openFilter,openOrigin) drive open state. Drops the manualposition: {top, left}math, thecontentHightcalculation, and the fixed 300×var height — the surface now sizes itself from content, and CDK Overlay's flexible-connected-position-strategy handles flip/shift when near a viewport edge. Same primitivemat-menuandmat-tooltipuse under the hood, so visual consistency with the+ Filtermenu comes for free. -
"+ Filter" → menu → pick: dialog now anchors to the new chip, not to
+ Filter. Earlier the dialog opened anchored to the+ Filterbutton'sCdkOverlayOrigin, but rendering the new chip pushed+ Filterto the right by the chip's width — so the dialog landed visually past the chip's right edge. Fix uses@ViewChildren('chipOrigin', {read: CdkOverlayOrigin})+ Angular'safterNextRenderhook: afterpendingFilterLabels.add(label)andcdr.markForCheck(), the deferred callback finds the chip whose label matches and uses its origin. Falls back toaddFilterOriginif the chip isn't in the QueryList yet (defensive). -
cdkOverlayOriginmoved from<app-button>to the chip wrapper<div>. The inner<app-button>isdisplay: inline-blockand its host's bounding rect collapses inside anitems-stretchflex parent — the origin point ended up at the bottom-right of the visible pill rather than the bottom-left. Anchoring on the wrapper (which has the visible border + bg) restored correct positioning. Click handler moved with it: clicking anywhere on the chip opens the dialog. The×inside the chip usesevent.stopPropagation()so it still only clears. -
Apply button enable/disable state.
canApply()returns true when: options-type has at least one selected; text-type hasis_empty/is_not_empty(no value required) OR has a non-emptytextValue. The Apply<app-button>binds[disabled]="!canApply()". Under OnPush,FormControl.valueChangesdoesn't auto-trigger CD, songOnInitsubscribes withtakeUntilDestroyed(destroyRef)and callsmarkForCheck()to keep the state reactive.applyOptions()also short-circuits inside ifcanApply()is false, so even if the disabled styling is bypassed the emit is a no-op. -
Enter to apply, two paths because of mat-select. For text-type filters,
(keydown)="onValueKeydown($event)"on the value<input matInput>checksevent.key === 'Enter'and callsapplyOptions()withpreventDefault+stopPropagation. For options-type filters,(overlayKeydown)on the CDK overlay (inFiltersComponent.onOverlayKeydown) handles Enter — checks the active filter is options-type, asksdialogComponent.canApply(), then commits + closes. The split avoids hijacking Enter when focus is on the condition<mat-select>trigger (mat-select uses Enter to open its dropdown). Initial attempt at(ngSubmit)on the<form [formGroup]>didn't fire reliably (Material widgets confuse the browser's implicit-submission heuristic), so we abandoned that route. -
SearchComponent's globaldocument:keydown.{arrowup,arrowdown,enter}listeners scoped to host. The@HostListener('document:keydown.enter', ['$event'])oncore/presentation/component/search/search.component.tswas firing for every Enter anywhere in the app whenever the search dialog was open — callingevent.preventDefault()unconditionally +openSelectedSearchResult(). Discovered while debugging Enter-to-apply in the filter dialog. Removed thedocument:prefix so the listeners only fire for events bubbling through the SearchComponent's own DOM. Captured the broader lesson in memoryfeedback_no_global_keydown_preventdefaultso we don't repeat the pattern. -
MatTooltip→ in-houseTooltipDirective. The Clear All filters button initially usedmatTooltip="Clear filters"; swapped to[tooltip]="'Clear filters'"againstWeb App/src/app/components/tooltip/tooltip.directive.ts(the project's existing CDK-Overlay-based tooltip). Same family of mistake as the Tailwind-vs-SCSS one — defaulting to Material primitives when an in-house wrapper exists. Captured in memoryreference_web_app_ui_primitives. -
+ Filterpicker UX shipped. Only "active" filters render as chips in the row; inactive ones live behind a+ Filterbutton that opens a<mat-menu>listing them. Adopting the Toggl pattern lets us scale the filter UI when a list has many possible filters without the row going wall-to-wall by default. Side effect of the Toggl "pending" idea: as soon as the user picks a filter from the menu, the chip appears immediately in the chip row and the dialog opens anchored to it — even before any state is set on the filter. If they dismiss without applying, the chip disappears viapendingFilterLabelscleanup incloseOverlay(). -
Clear All × button. 50×50px square (matches the chip / + Filter natural height) using
align-self: stretch(inherited from the row'sitems-stretch) — but with hardcodedwidth: 50px; height: 50px;infilters.component.scssbecauseaspect-ratio: 1 / 1on a flex item doesn't reliably override content-driven intrinsic width. Renders only whenhasActiveFilters()is true.clearAllFilters()resets every filter's state, clears the pending set, emitsfiltersUpdated. Tooltip on hover. -
Chip origin click + clear (×) is now a wrapper-level click; the inner
×button callsevent.stopPropagation()so the same click doesn't both clear and re-open the dialog. The chip's content<app-button type="none">no longer takes its own click handler. -
Process learnings worth memorialising:
tsc --noEmitdoes NOT run Angular's template type-checker. Usenpx ng build --configuration developmentfor Web App verification. Memory:feedback_web_app_verify_with_ng_build.- For Web App component styling, prefer Tailwind utility classes in the template over custom SCSS. Reserve SCSS for
::ng-deepoverrides and cases utilities can't express. Memory:feedback_web_app_tailwind_first. - Before reaching for
mat-tooltip/ rawmat-buttonetc., checkWeb App/src/app/components/for in-house primitives (TooltipDirective,<app-button>,<app-clipboard>, …). Memory:reference_web_app_ui_primitives. @HostListener('document:keydown.…')plus unconditionalevent.preventDefault()is a footgun — it blocks the key for every overlay/dialog. Always scope to the host element instead. Memory:feedback_no_global_keydown_preventdefault.
-
Build snag still flagged:
Client/Javascript Client/tsconfig.jsonoutputsdist/src/*instead of flatdist/*. Manualmv dist/src/* dist/ && rmdir dist/srcafter each build. Already inTODO.md.
Runner Pool header detail gets a tooltip + "Open runner pools" button
The flow overview's "Runner Pool" detail now shows a hover tooltip with the pool's configuration summary — name, source label ("Assigned to this flow" when the flow has an explicit override vs "Environment default" when it inherits), min/max runners, and CPU/memory limits when set — plus an "Open runner pools" button that takes you to the runners management page (/settings/resources/runners). Pattern mirrors the existing Code Source tooltip exactly. New RunnerPoolTooltipComponent + RunnerPoolWithData wrapper type; nav target via new EntityNavigationService.runnerPoolsList(). There's no per-pool detail page yet — the management list is the closest thing — so the button goes there. When a per-pool page eventually lands, only the nav helper changes.
2026-05-29
Edit-flow dialog now preselects the assigned runner pool
Opening an existing flow's edit dialog showed an empty "Runner Pool" picker even when the flow had a pool assigned. The flow-summaries list endpoint (GET /projects/:projectId/flowsummaries) — which the Web App uses to populate the list and pass the row object into the edit dialog — was dropping runner_pool server-side. FlowSummary.fromFlow() didn't copy runnerPoolId from its source Flow, and FlowSummarySqlRepository.parseRow() didn't read the column from the row even though flow.* brought it into the result set. The dialog's preselect lookup (pools.find(p => p.id === flow.runnerPoolId)) therefore always found nothing. Fixed by adding the missing field copy, the parseRow assignment, and the CAST(flow.runner_pool AS CHAR) as runner_pool alias on both SELECTs (BINARY columns must be cast to char like the sibling code_source / run_strategy). GET /flows/:flowId was unaffected — Flow.toJSON() already emits the field and the canonical FlowSqlRepository reads it correctly.
Autocomplete tolerates async-loaded options — no more setTimeout workaround needed
<app-autocomplete>'s writeValue used to error and null the form when called before [options] had been populated. The race is common: a dialog awaits an options list, calls signal.set(loaded), then form.patchValue({foo: matched}) in the same .then(). Reactive forms calls writeValue synchronously, but the [options] template binding only propagates on the next change detection cycle — so at write time, _options was still empty. Result: console error "Selected option is not a valid option, available options: Array(0)" and a silently-empty form control. Dialogs across the app worked around this with setTimeout(() => form.patchValue(...), 10).
Reworked the contract: writeValue now stores the value as pending and tries to resolve. If options aren't loaded yet, the value stays pending silently. setOptions calls a resolvePendingValue() step after updating _options, so any deferred value resolves the moment options arrive. The error path is preserved for the case where options ARE loaded and the value still doesn't match — that's a real mismatch worth logging.
- Removed
setTimeout(..., 10)workarounds in:edit-flow.component.ts(loadData),edit-code-source-page.component.ts(loadPlatforms/loadBuildStrategies/loadRunStrategies— 3 sites),edit-project-environment.component.ts(loadData). - New
loadRunnerPoolsinedit-flowwas always setTimeout-free; the autocomplete fix is what makes it actually work. - Other
setTimeoutusages (DOM positioning inflow-editor-page.component.ts, tooltip hide delays, auto-refresh timers) are unrelated and stay. - Edge case kept intentionally: if
_optionsis permanently empty AND there's a pending value, no error logs. Same visible state as before (empty placeholder), no spurious console noise.
Flow overview header shows the resolved runner pool
The flow overview page header now shows a "Runner Pool" detail next to "Code Source" with the pool that actually runs the flow — the flow's explicit override if set, otherwise the environment's default pool. Plain text, no tooltip yet (that can come when the pool detail page lands). The JS Client wasn't previously parsing ProjectEnvironment.default_runner_pool even though Library/Core was already serializing it on the wire; added the missing parse in attlaz-client ProjectEnvironment.parse. No server change needed. New InfrastructureService.getRunnerPoolById(id) wraps RunnerPoolEndpoint.getById() to keep the call shape consistent with the rest of flow-overview-page.
Code-source build-strategy display showed "Unknown build strategy"
CodeSourceHelper.getAllCodeSourcesWithData was searching runStrategies for a build strategy on line 64 of codeSourceHelper.ts — a copy-paste from the run-strategy block right above it. The resulting "Code source build strategy <id> not found" error logged on every code-source list load, and any UI that used the helper's enriched codeSource.buildStrategy (e.g. the autocomplete label in the flow edit dialog) showed "Unknown build strategy" even when the strategy existed. Fixed by searching buildStrategies.
Web App tables remember their state across navigation and reload
Paginated tables in the Web App now persist page size and cursor position in the URL. Going into a row's detail page and pressing Back lands you on the same window you left. Browser refresh stays on the cursor instead of resetting to page 1. Bookmarking a paginated URL works as expected.
-
Opt-in via the new
tableIdinput on<app-table>. Pages give each table a unique id; the table mirrors<id>_size,<id>_after,<id>_beforeto the URL query string. Multi-table pages (e.g. system / security with Blocked / Flagged / Rules; platform language details with Images / Build Strategies / Run Strategies) use distinct ids so the params don't collide. Default page size is omitted from the URL — only sizes the user actually picked surface. -
Rolled out to 40 pages / 44 tables. Flow runs, flows list, connections, notifications, events, price alerts / recommendations / strategies, products (competitor + enhance catalog), match jobs, competitor product list, content generation batches, restricted words, feeds, storage (overview / collections / buckets / objects / documents), members / projects / workspaces / identities / tokens / code sources / accounts / channels / devices / messages / subscribers, transformations, and the multi-table system / platform-language admin pages. Skipped intentionally: dashboard widgets, embedded reusable lists (
health-alert-list), and the layout demo page. -
Pagination buttons now correctly disable during loads instead of being clickable mid-flight. Clicking Next or Previous mid-load was silently rejected — buttons are
[disabled]="!hasX() || loading()". -
Background load failures show a toast instead of failing silently. If a
next()/previous()/refresh()errors while rows are already showing,NotificationService.showErrorfires with the error title/message. Initial-load failures still render<app-error>in the empty state as before — no double display. -
Cursor pagination accuracy fixes:
hasPreviousafter a URL-restored "next" window now correctly enables the Previous button (previously the URL-restored state disabled Previous because the data source only inspected the page direction, not the actual cursor).hasNextafter a backward navigation is now derived from whether the load actually returned data, instead of being hardcodedtrue— empty backward windows correctly disable Next. -
Reload preserves the cursor in the URL. Previously the page's Refresh button (and browser reload) reset the data source and the WRITE effect overwrote the URL to bare. The fix:
CollectionDataSource.connect()preserves an existingcurrentPagination, the WRITE effect skips when the URL already reflects state, and a multi-hookaddBeforeInitAPI runs URL-state-application insideinit()synchronously so reload always reapplies the URL cursor. -
<app-empty>default copy updated: empty state titles and extra text now flow throughemptyTitle/emptyExtrainputs consistently. -
Internal: 69 new test specs covering URL → ds, ds → URL, default-size omission, prefix collision, cursor regression cases, error / loading / empty state rendering, and cell template resolution (
column.fieldlookup +column.templateoverride + missing-template red fallback). Web App testbed gained its first non-trivial component tests; pattern documented for future test work. -
Memory leak fix as part of the cleanup:
CollectionDataSource._dataStreamwas an unboundedReplaySubject— every emitted page of rows was retained forever. NowReplaySubject(1)keeps only the latest. Also:setColumnsListpreviously mutated the host page'sTableColumn[]input array (assigningclassesonto the same column objects across renders); now shallow-clones first. Selection subscription is now correctly rewired when the dataSource input swaps (previously the old subscription was retained, missing data changes and leaking).
Uniform list-endpoint filtering — filter tree pilot on log streams
A second filter mechanism alongside the existing Stripe-style bracket helpers (?param[gte]=…, ?status=…). Same Library/Core/src/Attlaz/Model/Filter/ folder, two layered approaches: Tier 1 (existing single-field helpers) for boring lists, Tier 2 (filter tree under ?filter=<JSON>) for lists that need OR / same-field-twice / dynamic-key fields. Both compile down to the same FilterNode tree internally; repositories only see the tree.
-
Tree shape:
FilterNodeis a discriminated union of{and:[…]}/{or:[…]}/{not:…}/{field, op, value}— four node types, one canonical JSON shape that the Web App emits and the parser consumes directly. Operator union:eq/neq/in/not_in/lt/lte/gt/gte/is_null/is_not_null/starts_with/contains(12). Emptyin/not_inrejected at parse.is_null/is_not_nullvalue field is ignored (parser normalises tonull).starts_with/containsare case-insensitive. -
Allowlist with wildcard support.
FilterParser.parse(raw, allowedFields)validates field names per endpoint — exact matches plus trailing-.*prefix wildcards ('tag.*'allowstag.color,tag.flow_run_id, …). The allowlist is the boundary that protects against arbitrary-field injection into Mongo/SQL.FilterParseErroris rethrown asBadRequestErrorat the controller boundary via the newBaseRequestHelper.getFilter(allowedFields). -
FilterToMongoTranslatorwith the gotchas baked in:is_null→{$type: 10}(BSON null type —{field: null}would silently match missing fields too),is_not_null→{$exists: true, $ne: null},starts_with/containsescape regex metachars,not→$nor(Mongo has no document-level$not). -
Dynamic tag fields via
tag.<key>convention. Compare nodes whose field starts withtag.target thetags: {key, value}[]array convention used by log entries — the translator emits$elemMatchontagswith the extracted key. Lets the Web App pass{field: 'tag.color', op: 'in', value: ['green', 'blue']}and have it mean "color is green OR color is blue", and lets(tag.color in [green,blue]) AND (tag.size eq big)compose under$andcleanly.is_not_nullontag.<key>means "entry has at least one tag with this key";is_nullmeans "entry has no tag with this key". -
Side-effect bug fix: the previous
LogMongoDBRepository.getPaginatedResultflattened a multi-value tag filter into{tags: {$all: [{key,green}, {key,blue}]}}—$allrequires every listed element to be present, so selecting two values within one key returned empty unless an entry happened to be tagged both. Real-world traffic stayed in the single-value-per-key case and the breakage was latent. The$elemMatchtranslation makes "color in [green, blue]" return what the UX promises. -
Breaking — log stream endpoint contract.
GET /logstreams/:streamId/logsno longer reads?filter_log_levels=or?filter_tags=. Equivalent filtering moves under?filter=<JSON>:- Levels:
?filter={"field":"level","op":"in","value":["error","warn"]} - Tags:
?filter={"field":"tag.color","op":"in","value":["green","blue"]} - Combined:
?filter={"and":[{...},{...}]} - Current allowlist:
['date', 'level', 'message', 'tag.*']. Cursor pagination +stream_idstill bound the scan, so no Sentry-style mandatory date bound is enforced.
- Levels:
-
Breaking — Library/Core
LogRepository.getPaginatedResult(streamId, pagination, logItemFilter?)→getPaginatedResult(streamId, pagination, filter?: FilterNode | null). All four implementations (Mongo / Mock / Elasticsearch / Api) updated.LogItemFiltermodel deleted. The integration specLogQueryAndPaginationTest.spec.tsmigrated to the new shape and gained a "same-key OR plus cross-key AND" case. -
Breaking — JS Client
attlaz-client.LogEndpoint.getCursoredLogEntries(streamId, pagination, filter)now takesFilterNode | nulland emits?filter=<JSON>instead of the old delimited query string.LogItemFilterclass removed; package now exportsFilterNode/AndFilterNode/OrFilterNode/NotFilterNode/CompareFilterNode/FilterValue/FilterPrimitive/FilterOperatorfrom./Model/Filter/. (Perfeedback_js_client_enum_duplication, the types are duplicated betweenLibrary/Coreandattlaz-client— both need to ship together.) Consumers usinggetCursoredLogEntries(…, new LogItemFilter())must construct aFilterNodeinstead. -
Web App pending —
log-list.component.ts:filtersToLogItemFilterstill emitsLogItemFilter. Migration step: rewrite to emitComparenodes (field: 'level'orfield: 'tag.<key>'), wrap multiple selections within a key in{or:[…]}and across keys in{and:[…]}, pass the resultingFilterNodetolog.service.ts:getCursoredLogEntries. Dynamic tag-key discovery via/logstreams/:streamId/infois unchanged. -
Deferred: SQL translator (Sentry-style ES translator explicitly out of scope until a SQL list endpoint adopts the tree); a
GET /:resource/_filter-schemaendpoint that lets the Web App fetch filter capability dynamically (introduce when a second endpoint duplicates its allowlist). Tier-2 operators (ends_with,matches,full_text, array ops,exists) deliberately left out — add per-endpoint when asked. Plan doc:~/.claude/plans/attlaz/uniform-filtering.md. -
Cleanup: orphaned Tier 1 scaffolding removed in the same pass —
Library/Core/src/Attlaz/Model/Filter/DateRangeFilter.ts,SearchFilter.ts, andBaseRequestHelper.getDateRangeFilter()had zero consumers across the platform. Date ranges are now expressed as twoComparenodes (gte+lte) under the filter tree; free-text search as acontainsCompare on the relevant field. The remaining Tier 1 helpers (getStringFilter/getEnumFilter) keep their value as thin wrappers aroundquery.getOptionalString/getOptionalValueEnum.
Filter follow-ups: Web App migrated, Toggl-style chip UX, server-side Context, ends_with
Second pass on 2026-05-29 closed the loop on the items the morning entry flagged as deferred (Web App migration) and added several UX improvements driven by the Toggl filter UX as the reference.
-
Web App
log-listfully onFilterNode.log-list.component.ts:filtersToFilterNodeemitsComparenodes for log level ({field:'level', op:'in', value:[…]}), tags ({field:'tag.<key>', op:'in', value:[…]}), and Context ({field:'context', op:<condition>, value:<text>}). The pre-existing bug whereloadYoungerLogEntries/loadOlderLogEntries(Refresh + infinite scroll) bypassed the active filter is also fixed — both now pass the filter through. Log level options are sorted by severity (Emergency → Debug) viaLogLevel.compare. -
Filter model: new
textCondition: TextFilterCondition | nullfield on the Web AppFilterclass. Eight conditions:contains,does_not_contain,starts_with,ends_with,is,is_not,is_empty,is_not_empty. Default for fresh text filters iscontains. -
New universal operator
ends_withadded to the filter tree (FilterOperatorin both Library/Core andattlaz-client). SQL maps toLIKE '%foo'(translator pending); Mongo emits{$regex: 'foo$', $options: 'i'}with metachar escaping. Used by the text filter UX directly. -
Server-side Context —
field: 'context'added to the log-stream allowlist and toFilterToMongoTranslatoras a special case.contextis a nestedRecord<string, unknown>so plain$regexagainst it never matches; the translator uses$expr+$functiontoJSON.stringifythe field per document and regex-match the stringification.is_null/is_not_nulltake a faster path ({$or: [{context: null}, {context: {$eq: {}}}]}). Cost caveat:$functionruns server-side JavaScript per matched doc — fine for log streams bounded bystream_id+ cursor pagination, will not scale to cross-stream search. Tracked in memoryproject_context_filter_cost_reviewfor re-evaluation. -
Client-side
FilterLogsPipe+searchText()deleted from the Web App now that Context is server-side. Refresh / pagination respect Context consistently because all entries return from the server pre-filtered. -
Toggl-style chip labels in
FiltersComponent:- Options, single selection:
"Log level is error". - Options, multi selection:
"Log level is one of 3 selected". - Text:
"Context contains foo"/"Context is empty". Label generation inFiltersComponent.chipLabel(filter).
- Options, single selection:
-
"+ Filter" picker — only active filters render as chips; inactive filters live behind a
+ Filtermenu (mat-menu). A filter is active when any option is selected or a text condition is set. Adopting Toggl's discoverability pattern over Attlaz's previous always-on-rows pattern; works because log streams have a small filter count. For lists with many possible filters (e.g. future member list), this UX scales — the always-on pattern wouldn't. -
× on chip clears the filter, returning it to the
+ Filtermenu. Faster than opening the dialog and deselecting everything. -
FilterValueSelectDialogComponentextended to render a Conditionmat-selectfor text filters; the value input hides when the condition isis_empty/is_not_empty. -
products-page.component.ts(app-competitor-monitoring) updated to includetextCondition: nullon everyFilterliteral — non-breaking, was the only other consumer of the Filter model. -
JS Client build snag flagged:
Client/Javascript Client/tsconfig.jsondoesn't pinrootDir, sotscoutputsdist/src/*instead of flatdist/*. Thepackage.jsonexportsmap expects flat. Manual workaroundmv dist/src/* dist/ && rmdir dist/srcafter build. Tracked in TODO.md "Planned Improvements". -
Memory + plan updates:
project_uniform_filteringnow documents what shipped + follow-ups; newproject_context_filter_cost_reviewrecords the cost trade-off and re-evaluation triggers; TODO.md "Planned Improvements" gets entries for date/number filter types, reference-field dynamic options, URL persistence, schema endpoint, SQL translator, and the JS Client build snag.
2026-05-25
Adapter "Test connection" button — migrated from flow-run to Services-Api
The Web App's "Test" button on an adapter connection used to spawn a synchronous flow run against a hardcoded flow (25tEUt5z7ic1JcQvPJWVihgzWXy) in the Attlaz System project, whose PHP command (Project/Attlaz-Project-System/src/App/Command/CheckConnectionStatus.php) essentially called $connection->getInfo() per adapter type. Replaced by a per-adapter testConnection() method exposed via POST /services/:adapterConnectionId/test-connection in Services-Api. Same code path now serves both the user-triggered Test button and the Supervisor periodic health check that produces HealthTestType.ADAPTER_CONNECTION_IS_HEALTHY alerts every 15 minutes — previously two separate code paths covered the same probe with different fidelity.
-
ExternalConnectorinterface generalized.getInfo(accessToken: AccessToken)→getInfo(ctx: AuthContext); addedtestConnection(ctx: AuthContext): Promise<TestConnectionResult>.AuthContextis a discriminated union over the four auth flavours the platform uses (accessToken/apiKey/sshCredentials/hueCredentials). Every Services-Api connector now implements both — including the previously non-OAuth ones (SshConnector,OpenAIConnector,GarminConnector,BanqUPOAuthConnector,SlackOAuthConnector,GoogleSocialConnector) that didn't havegetInfobefore. Narrowing helpersexpectAccessToken/expectApiKey/expectSshCredentials/expectHueCredentialskeep connector implementations clean without sprinklingascasts. -
State-based result shape, not a boolean.
TestConnectionResultcarriesstate: 'passed' | 'warning' | 'failed' | 'skipped'(mechanically derived as the worst per-check state —skipped < passed < warning < failed), an array ofchecks(each with name / state / durationMs / optional detail), and an optional top-levelerror. Discarded theok: booleanshape because it conflated "did the test run" with "did everything pass" — the four-value enum aligns with the existingHealthAlertStatusand the open Severity Separation plan. -
Schema validation is the first
checks[]entry.ValidateConnectionConfigurationCommand(the existing "required field present" check that produced today's "No value for X" alerts) runs first inConnectionTestController. If schema fails, subsequent network probes are markedskippedand not run — keeps the alert pipeline behaviour identical for config-broken connections. -
Per-connector probes. Most OAuth connectors use the new
runHttpProbe(name, url, init)helper to call a cheap authenticated endpoint (/v1/meon Spotify,/useron GitHub/GitLab/Bitbucket,/userinfo/v2/meshared across the 5 Google variants, etc.). Connectors with non-OAuth auth roll their own: Hue probes the bridge endpoint and assertsapplication_keyis present (two checks); SSH does TCP connect +exec true(two checks); OpenAI hits/v1/models; Garmin callsgetRecentActivities(limit=1). BanqUP returns'failed'with'Not implemented'since no working API call exists yet. -
Supervisor parser updated.
Infrastructure/Supervisor-Service/src/Command/CheckAdapterConnectionsCommand.tsnow POSTs to/services/:id/test-connectionand reads the state-based response:'passed'/'warning'→ PASS,'failed'→ FAIL (issues collected from per-check details),'skipped'→ SKIP, malformed → SKIP. HealthAlert mapping unchanged. Warnings currently map to PASS untilHealthAlertSeveritysupports a richer WARN flow (tied to the open Severity Separation plan). -
Web App "Test" button now renders a step-by-step view.
check-connection.component.htmlshows the overall state with a colouredapp-statebadge (green/yellow/red/gray for passed/warning/failed/skipped) and lists each check below: name, OK/Failed/Skipped, duration in ms, and any detail message. A user testing a Spotify connection sees✓ Configuration valid · ✓ GET /v1/me (147 ms); a revoked-token Spotify connection sees✓ Configuration valid · ✗ GET /v1/me (89 ms) — HTTP 401. -
attlaz-client1.62.0 publishesAdapterConnectionEndpoint.testConnection(connectionId): Promise<TestConnectionResult>along with theTestConnectionResult/TestConnectionCheck/ConnectionTestStatetypes, all exported from the package root. -
Known regression — flow-run adapters lose the Test button. Attlaz has two architecturally distinct adapter kinds: service adapters (live in
Api/Services-Api/src/Service/, exposed via HTTP) and flow-run adapters (PHP / Node libraries installed inside the flow runtime — Magento2, SQL, SFTP, SSH-PHP). The legacy flow's PHPgetConnectionClassname()switch covered the flow-run kind. After the migration the Test button is a service-adapter-only capability; clicking Test on a Magento2 connection returnsstate: 'skipped'with'Test not supported for this adapter type'. Those adapters re-gain the button if/when they're ported to Services-Api. The Supervisor periodic check produces SKIPPED alerts for them instead of FAILED — which is actually more honest ("we can't test this" vs "we know it's broken"). -
Deprecation cleanup. Removed Core-Api
AdapterConnectionController.validateConnection(thePOST /connections/:id/checkroute that spawned the flow run), Services-ApiConnectionValidationController(thePOST /connections/:id/validateroute the Supervisor used previously), and the legacy PHP command files inProject/Attlaz-Project-System/src/App/Command/CheckConnectionStatus.php+src/App/Model/CheckConnectionStatusResult.php. The Attlaz System flow25tEUt5z7ic1JcQvPJWVihgzWXyis deleted from the database.
Garmin connector — 11 MCP operations for training, sleep, and recovery analysis
Garmin Connect joins Hue / Google Sheets / OpenAI / Ssh / Strava as the fifth MCP-exposed connector. Read-only, stability: 'beta', category: 'fitness'. Covers activities, heart rate, sleep, daily steps, and weight — every metric exposed in both a single-day form and a date-range form. Lives at Api/Services-Api/src/Service/GarminConnector.ts; library wrapper at Api/Services-Api/src/Adapter/Garmin/GarminClient.ts. 11 @ConnectorOperation methods: getRecentActivities, getActivity, getActivitiesInRange, getDailySteps, getDailyStepsRange, getSleep, getSleepRange, getHeartRate, getHeartRateRange, getWeight, getWeightRange.
The Garmin landing was the test bed for two adapter-design patterns that will roll out to other connectors over time: include: 'summary' | 'full' on heavy single-day tools (summary strips Garmin's per-minute arrays — 51× payload reduction on sleep, 40× on HR, 2.7× on activities), and paired range tools that return one summary row per day across a date window (turns "last 3 months of sleep" from N daily calls into 1).
-
Library choice — supply-chain audit caught an obfuscated fork. Initially picked
@gooin/garmin-connect@1.8.0for its bespoke AI-coach methods, then audited and walked away: dist JS was deliberately obfuscated (name-mangled with string-array decoder, not minification), no source maps, anonymousgooin@outlook.commaintainer,repository.urlin package.json pointed to an upstream that didn't contain any of the published code. Unacceptable for a package that handles OAuth tokens. Switched to@flow-js/garmin-connect@1.6.16(florianpasteur fork): readable tsc-compiled JS with source maps, named maintainer, public GitHub repo. Lost the coach methods (training_overview_v1 / wellness_overview_v1 / activities_summary_v1 — @gooin-only) but gained auditability. The four-check audit pattern (obfuscation / source maps / maintainer / repo) is now memory entryfeedback_npm_supply_chain_audit. -
Auth model — local minting, ProviderToken storage with refresh disabled. Garmin has no public OAuth program for third-party apps; the @flow-js library impersonates the official Garmin Connect mobile app via reverse-engineered SSO. We do not run that SSO in the deployed Services-Api. Instead:
npm run garmin-mintreadsGARMIN_USERNAME/GARMIN_PASSWORDfrom.env, runs the SSO locally, creates or updates a ProviderToken row withtype: AdapterConnection,expires_at: NULL, OAuth2 access+refresh tokens in the standard columns, and writes the ProviderTokenId into the connection'saccessconfig field (typeoauth:garmin). ConnectorManager then resolves it via the standardauthentication: 'accessToken'path — Garmin needed zero custom code in ConnectorManager. Settingexpires_at = NULLis deliberate:ProviderTokenSqlRepository.getExpiring()filtersWHERE expires_at <= now+6h, so the Provider-Token-Service refresh cron silently skips Garmin tokens (no refresh handler exists for the impersonated client_id). When the access_token actually expires (~24h), MCP calls 401 and the user re-runsgarmin-mint— same ProviderToken row updates in place, no Web App re-paste. -
Skipping OAuth1 at runtime. Garmin's library API requires an OAuth1 consumer-token pair + OAuth2 access/refresh pair; both come out of the SSO. But: the library only ever uses OAuth1 to refresh the OAuth2 token (signing the refresh exchange against diauth.garmin.com). All actual data calls against connectapi.garmin.com use OAuth2 Bearer. So
GarminClient.buildGc()passes a dummy OAuth1 + the real OAuth2 withexpires_at: Number.MAX_SAFE_INTEGER— the library's pre-emptive refresh check (oauth2.expires_at < now) never fires, and OAuth1 stays untouched. If Garmin actually rejects the access_token with 401, the library tries to refresh and fails with a recognisable error — that's the signal to re-mint. This lets us use the standard single-tokenAccessTokenwrapper instead of inventing a new auth type. -
Tool design —
include: 'summary' | 'full'toggle on heavy ops. Garmin's raw daily sleep response includes per-minute movement, HRV, respiration, body-battery, and stage arrays — ~130 KB per night. Daily HR includes a ~700-point time series (2-minute sampling) — ~13 KB. Most analysis only needs the summary fields.getHeartRate,getSleep,getRecentActivities, andgetActivityeach take anincludeenum, default'summary'. Summary mode strips known-heavy arrays / known-noise fields and keeps the rest. Verified payload sizes: sleep 132.70 KB → 2.60 KB (51×), HR 12.75 KB → 0.32 KB (40×), activities at limit=5 20.66 KB → 7.59 KB (2.7×). Tool descriptions explicitly explain the toggle so an agent doesn't accidentally pull full mode across 30 dates. -
Activity noise blocklist.
ACTIVITY_NOISE_FIELDSinGarminClient.tslists ~45 universally-useless fields stripped in summary mode: 5 owner avatar URL variants,userRoles,privacy, all thehas*capability flags (hasIntensityIntervals,hasHeatMap,hasSplitsetc.), sport-irrelevant fields (summarizedDiveInfo/qualifyingDive/decoDiveon running activities,summarizedFloorsClimbedetc.), redundant timestamps (beginTimestamp,endTimeGMT), and internal IDs (activityUUID,timeZoneId,deviceId,manufacturer). Blocklist (vs allowlist) was chosen so new Garmin fields survive without us touching the code. Diagnosed by reviewer feedback after limit=50 blew the context budget — 50 × 85 fields ≈ 200 KB; with stripping ≈ 76 KB. -
Range tools — one summary row per day.
getHeartRateRange,getSleepRange,getDailyStepsRange,getWeightRange,getActivitiesInRange. Each returns{summaries: [{date, ...}, ...], startDate, endDate}. Verified sizes for a 7-day window: HR 0.87 KB, Sleep 1.34 KB, Steps 0.30 KB, Weight 3.85 KB, Activities 3.18 KB. For "analyse my last 3 months" workflows, the total context cost is now ~50 KB across 4 range calls instead of hundreds of KB of fan-out from the daily tools.getActivitiesInRangereturns a more aggressive compact shape (~14 essential fields per activity instead of stripped-but-still-rich ~50) — designed for trend analysis where the agent shouldn't pull every per-activity detail. -
Native server-side range endpoints — Steps and Activities. Most range tools fan out N parallel daily calls under the hood (Garmin has no native range for HR or Sleep, verified via probe script). Two metrics have native server-side range endpoints we use directly: (1)
getDailyStepsRangehits/usersummary-service/stats/steps/daily/{start}/{end}— one call, returns array of{calendarDate, totalSteps, totalDistance, stepGoal}(the distance + goal are bonus fields the per-day endpoint doesn't expose). (2)getActivitiesInRangehits/activitylist-service/activities/search/activities?startDate&endDate&limit&start— server-side date filter plus pagination loop (200 per page, up to ~10,000 activities) so decade-scale ranges work without truncation. Both raise the agent-facing range cap toMAX_NATIVE_RANGE_DAYS = 3650(~10 years). -
Garmin's undocumented 28-day server cap on stats/steps/daily. The native steps endpoint rejects ranges wider than 28 days with
400 Bad Request "cannot be more than 28 days". Discovered when a reviewer's Claude session trusted the documented 3650-day cap and got a hard failure at 29 days. Fix: transparent client-side chunking.chunkDateRange()helper splits any range into ≤28-day windows, fires them in parallel viaPromise.all, concatenates per-day rows. Agent still makes one logical call. A 1-year request fans out into 14 chunks, a 10-year request into 130. Memory entryreference_garmin_server_capsdocuments the cap (and the 404s for the HR / sleep / multi-day-summary candidate endpoints that don't exist). The audit script is atApi/Services-Api/src/Adapter/Garmin/verifyGarminRanges.script.ts(npm run garmin-verify-ranges). -
Chunking infinite-loop bug — caught and fixed. Initial
chunkDateRange()implementation usedcursor.setUTCDate(chunkEnd.getUTCDate() + 1)to advance to the next chunk, which only mutates the day-of-month — cursor went backwards whenever a chunk crossed a month boundary, generating infinite chunks → infinite parallel HTTP → OOM crash at 4GB. Fix:cursor.setTime(chunkEnd.getTime()); cursor.setUTCDate(cursor.getUTCDate() + 1);to preserve month/year. Caught by the 90-day verification step (which the original smoke test lacked — only ever ran 7-day windows, never tested near-cap). Feedback memoryfeedback_verify_near_cap_empiricallycaptures the broader lesson: when documenting limits, test at 90-110% of the cap, not just sample sizes. -
Library quirks worth knowing. Documented across
GarminClient.tsfor the next maintainer: (1) GarminConnect constructor's TYPE says credentials are optional but the RUNTIME throws "Missing credentials" if either is absent — pass{username: 'token-only', password: 'token-only'}dummies. (2) Several methods are present at runtime but missing from the published.d.ts(getWeightRangewas even in the README but doesn't actually exist) — verify withObject.getOwnPropertyNames(Object.getPrototypeOf(gc))before adding type-cast augmentations. (3) Library type filesdist/garmin/types/heartrate.d.tsandweight.d.tsare ambient declarations withoutexport— can't beimported across package boundary; inline minimal types instead. (4)skipLibCheck: trueis now set inApi/Services-Api/tsconfig.jsonbecause the library's.d.tshas unresolved type references (WeightData,HeartRateetc. used but not imported). All four pitfalls captured inreference_garmin_library_choice. -
JSDoc landmine —
**/ends a comment block. Hit while writing thetsconfig.jsonexclude pattern in a comment header:\src//test*.ts`looked harmless, but thefollowed by/contains*/which terminates the JSDoc block mid-sentence; subsequent lines parsed as code, errors everywhere. Reworded to avoid the sequence. Memory entryreference_jsdoc_double_star_slash_gotcha` so the next person catches it faster. -
Developer ergonomics — five new npm scripts in Services-Api:
garmin-mint(SSO + ProviderToken create/update + connection link),garmin-test(smoke test all 11 ops including summary/full size comparison + 90-day chunking verification + near-cap probes for HR/Sleep/Weight),garmin-inspect(dumps the adapter's config-field schema + the connection's current values for debugging),garmin-verify-ranges(probes candidate native range endpoints to confirm which work, returns sizes and previews — used during the native-endpoint investigation). All read./garmin-tokens.json(gitignored) whichgarmin-mintwrites alongside the DB updates. -
Known unfinished work: (1) Server-side refresh handler for
/app/garmin/refreshinOAuthController.tsso the 15-min Provider-Token-Service cron can renew access tokens automatically — currently the user re-mints every ~24h. (2)bucketBy: 'week' | 'month'on the per-day fan-out range tools (HR, Sleep, Weight) so decade-scale trend analysis becomes trivial — current 92-day cap means a 10-year sleep query needs 40 fan-out chunks. (3) Credentials-exchange UI in the Web App so users mint tokens server-side (entering username/password once into a form, password discarded after first successful mint, only tokens stored). Today minting is local-CLI-only. (4) Garmin SSO is reverse-engineered and fragile — expect occasional library updates following Garmin's auth changes; not a "ship and forget" integration.
Supervisor flow-success monitoring restored + flow_idx / flow_run_idx schema cleanup
The Supervisor's per-env flow-success check (CheckIfFlowsAreSuccessFul) was silently failing for any project environment containing a flow that had never run. FlowSummarySqlRepository.getSummariesByProjectEnvironment uses a LEFT JOIN flow_idx ... OR flow_idx.project_environment IS NULL to surface never-run flows in the Web App; the row parser at FlowSummarySqlRepository.ts:177 didn't tolerate the resulting null project_environment, threw, and the supervisor's outer try/catch turned the throw into an empty CursorResult — so the entire flow-success check for the affected env was skipped silently. Visible symptom: Invalid entity id: is null {"id":null,"type":"ProjectEnvironmentId"} errors in Supervisor logs; invisible symptom: no flow-failure alerts for the affected projects.
- Parser now tolerates the documented null case.
parseRowtakes an optionalprojectEnvironmentFallbackId: ProjectEnvironmentId | null = nullparameter; whenrow.project_environmentis null the parser falls back to the queried env id supplied by the caller via a one-line lambda at each call site. Throws with a clear message if a null appears without a fallback — invariant violation becomes observable instead of a silent crash. - Column-shadowing bug fixed in the same parser. The
SELECT flow.*, flow_idx.*, ...shape meant MySQL's last-column-wins rule hadflow_idx.created_at/updated_atoverwritingflow.created_at/updated_atin the row. Both queries now aliasflow.created_at AS flow_created_at+flow.updated_at AS flow_updated_atand the parser reads the aliases.FlowSummary.created/.updatednow mean "when was the flow created" regardless of idx-row state. - Schema cleanup on both indexation tables. Dropped
flow_idx.idx_updatedandflow_run_idx.idx_updated(both had schema comments marking them for removal; the only historical reader was retargeted toidx_updated_atfirst). Renamedflow_run_idx.code_deploy→flow_run_idx.deploy(the field-level TODO "rename this to deploy" has been outstanding for a while). Droppedflow_run_idx.project_deploy(zero remaining writers;flow_run.project_deployon the canonical table is unaffected and still in use). Wire format unchanged — REST already exposed the field asdeploy. Migrations:Library/Core/database/2026-05-25-drop-idx-updated.sql+Library/Core/database/2026-05-25-flow-run-idx-rename-deploy.sql. - Out of scope (intentionally deferred). The longer-term fix is eager
flow_idxrow creation (one row per(flow, env)at the moment the pair becomes valid) so the LEFT JOIN allowance can be dropped and the parser becomes null-strict. Discussed at length, drafted as an event-driven and then command-object design, and parked — the defensive parser handles the case correctly today and the structural refactor is a separate plan worth sizing on its own merits. - Lib bump: Library/Core 1.197.0 is the source-of-truth release. Consumers redeployed: Core-Api, Supervisor-Service, Event-Service, Command-Service, Worker, Maintenance-Service, Products-Api. SQL migrations + service deploys went out together to avoid the no-compatible-middle-state during the
code_deploy→deployrename.
Bug fix: FlowRunStatsController now accepts nullable Worker stats payloads
Worker started POSTing file_read: null / file_write: null to /flowruns/:id/stats after the dockerode migration (changelog 2026-05-20) — ProcessStatsHelper.ts reads file I/O bytes from Docker's blkio_stats.io_service_bytes_recursive, which is empty on hosts running cgroup v2 (the default on the platform's k3s runner nodes). CPU/memory/network are populated from other parts of the stats payload that cgroup v2 does fill, so only file I/O surfaces as null. FlowRunStats.ts already declared all eight stats fields as number | null = null; FlowRunStatsController.getStatsFromBody was still calling api-lib's strict getValueNumber which throws 400 on null. Switched all eight fields (memory, memory_limit, cpu, cpu_limit, network_outbound, network_inbound, file_read, file_write) to getOptionalValueNumber so the controller validation matches the model's nullability. Worker stats inserts succeed again. The chain — dockerode + cgroup v2 + nullable file I/O — is captured in memory project_worker_dockerode_cgroup_v2_stats for the next time someone wonders why those columns are null.
2026-05-23
Strava connector — 8 MCP operations for training-history analysis
Strava was a skeleton: OAuth wired up, getInfo for the connect-flow display name, zero callable operations. Now exposes a coaching-focused surface that lets an MCP client analyse activity history over years — not just list it.
- 8 new
@ConnectorOperationmethods inApi/Services-Api/src/Service/StravaConnector.ts:listActivities(paginated history, before/after/page/perPage filtering up to 200/page),getActivity(full DetailedActivity with optionalinclude_all_efforts),getActivityStreams(second-by-second sensor time-series — HR, power, cadence, pace, GPS, altitude, etc.; keyed-by-type response),getActivityZones(time-in-HR/power-zone distribution per activity),getActivityLaps(per-lap breakdown for interval workouts),getAthlete(full profile with weight/FTP/gear),getAthleteStats(recent/YTD/all-time totals per sport, athlete id resolved automatically),getAthleteZones(HR/power zone boundary definitions). Alleffect: 'read',category: 'fitness', standardaccessTokenauthentication. - Streams / zones / laps are the load-bearing additions that make actual training analysis possible — HR drift, normalised power, intensity factor, polarised-training distribution, interval target adherence. Other Strava MCPs in the wild expose activity listings + segments but skip the time-series and zone data, which is where the diagnostic signal lives.
- New
StravaClient(Api/Services-Api/src/Adapter/Strava/StravaClient.ts) — stateless static methods, one per Strava v3 endpoint. Centralises Bearer header, request building (URL-encoded query params), and JSON-aware error mapping (Strava returns{message, errors}on failure; the wrapper surfaces both in thrown errors so callers see the real cause). - OAuth scopes tightened —
dataScopeswent from['read_all']to['activity:read_all', 'profile:read_all']. The oldread_allvalue only covered private segments/routes and would have 401'd every operation above. Existing Strava connections need to click Authenticate again to get a fresh token with the new scopes. Code comment at the scope declaration flags a known gap for follow-up: Strava docs strictly say comma-delimited scopes butOAuthController(scopes.join(' ')) emits space-delimited; Strava accepts both in practice so the deferred fix is only triggered if a re-auth ever returnsBad scope. - Deliberately NOT included: segment ops (
exploreSegments,getSegment,getSegmentEfforts,listStarredSegments) — segments are for PR/KOM hunting, not coaching; 4 more methods following the same pattern when needed. Write ops (updateActivity) — coaching goal is read-only.responseSchemaongetActivity/getActivityStreams/getAthlete— Strava's response shapes are large with optional+variant fields by sport_type or stream-key set; raw passthrough preserves richer fields without us re-modelling per release.
Services-Api: sync-catalog / list-catalog npm scripts (build workaround)
The documented npm run build && node --env-file=.env dist/Catalog/syncOperationCatalog.script.js instruction was a silent no-op: tsconfig.json excludes src/**/*.script.ts alongside test files, so the regular build never produced dist/Catalog/*.script.js and the node step failed with module-not-found. Fixed properly:
- New
Api/Services-Api/tsconfig.script.json(checked in) — extendstsconfig.json, drops only the*.script.tsexclude. - New npm scripts:
npm run sync-catalogreconcilesattlaz.adapter_operationwith the current decorator surface;npm run list-catalogprints the catalogue grouped by adapter with PLANNED entries first. Both wrapnpm run prebuild && tsc -p tsconfig.script.json && node --env-file=.env dist/Catalog/<script>.script.jsso the version-info emit + script compile + execution chain happens in one command. - Pattern is reusable for any new
.script.tsin Services-Api — same shape applied to in-progress Garmin work (npm run garmin-login,npm run garmin-test). - Connectivity requirement unchanged: the script reaches the platform MariaDB at
infrastructure-r8s62.attlaz.com, which is firewalled. Run from a Tailscale-connected workstation orkubectl execinto a Services-Api pod; ETIMEDOUT from a bare dev shell is a network issue, not a code bug. - Docs updated at
Api/CLAUDE.mdand at the top ofsyncOperationCatalog.script.ts/listOperationCatalog.script.tsto point at the new commands and call out the Tailscale requirement.
2026-05-20
Hue OAuth end-to-end from the Web App — post-connect hook persistence fixed
Clicking "Authenticate" on a Hue connection in the Web App now completes the full OAuth → remote bridge-link-button → application-key registration handoff without any manual DB intervention. Five separate issues were exposed during the first real end-to-end test, all fixed:
saveConnectionConfigurationis REPLACE-ALL (Library/Core/src/Attlaz-Connection/Repository/Sql/AdapterConnectionSqlRepository.ts:99-138). It iterates everyadapter_configurationrow for the connection's adapter and writes each one — fields not in the input list getNULL.OAuthController.handleAuthenticateConnectionwas calling it twice (post-connect extras, then access), so the second call wiped theapplication_keythe first one had written. Fix: collect post-connect extras + the access row into a single list and save once. Comment in the controller flags the footgun for future callers; memory entryfeedback_save_connection_config_replace_allcaptures it as a lessons-learned rule.- Post-connect field-key → KSUID resolution (
Api/Services-Api/src/Controller/OAuthController.ts).PostConnectHook.onPostConnect()returnsRecord<string, string>keyed by the human field name (e.g.application_key); the controller was wrapping that string innew AdapterConfigurationId(...)instead of looking up the seeded row's actual KSUID viaadapterRepository.getAdapterConfiguration(). The bad writes either silently no-op'd or landed on phantom rows. Now: fetch the adapter's configurations once, build akey → AdapterConfigurationmap, resolve each post-connect field through it. Unknown keys log[OAuth] Post-connect returned unknown field keyand skip rather than silently corrupting. - Diagnostic logging on the post-connect path:
[OAuth] Running post-connect hook/Post-connect hook returned(withfieldKeys) /Saving connection configurations(withcount). Future failures are visible without instrumentation. HueConnector.onPostConnectnow validates the Hue v1 response shape. The Hue Remote API returns HTTP 200 even on failure — outcome lives in the body as[{"success": {...}}]or[{"error": {...}}]. Step 1 (PUT /bridge/0/configlinkbutton activation) previously discarded the response entirely; failures surfaced later as a generic "unable to obtain application key" throw with no actionable detail. Both steps now check HTTP status + the in-body error shape and throw with the real description (most common: type 101 "link button not pressed").- Web App
OauthPlatformService.authAppparses/prepare-connectresponses properly. Theas { data: { state: string } }cast was a type lie — the API returns{state}raw (DataResult sends the body unwrapped). The cast silently producedundefined, the URL built withundefinedas the state code, and the popup never opened (no exception visible — caught silently in the auth-service-provider's.catch). Replaced the cast with a smallparseStateCode()runtime parser that validates each step (object → hasstate→ non-empty string) and logs a specific[OauthPlatformService]error message on each failure mode.
Companion UX todo (not addressed in this round): OAuth popup error handoff — when OAuthController.handleInitiate/handleCallback errors, the popup renders raw JSON instead of using HtmlOutputFormatter.popupResponse({error: ...}) to postMessage the error back to the opener. Captured as memory project_oauth_popup_error_handoff.
MCP follow-up bundle: server instructions, structured errors, Hue colour round-trip + device-name fix
Same-day follow-up to the MCP tools/list quality pass, driven by two more rounds of Claude Desktop reviewer feedback.
- Server-level
instructionsininitializeresponse (MCP 2025-06-18). NewApi/Services-Api/src/Model/AdapterInstructions.tscarries per-adapter content;McpController.handleInitializefetches the adapter for the connection and emitsinstructionswhen present (omits the field for adapters without content). Initial content shipped for all four active adapters (Hue, Google Sheets, OpenAI, Ssh), each covering workflow, gotchas, and explicitly what isn't supported yet — so the agent can answer honestly when users ask. Why this matters more than outputSchema/annotations: the reviewer empirically verified that Anthropic's MCP client layer strips per-tool structured fields (outputSchema,annotations) before they reach the model — across multiple servers, including Atlassian Rovo — butinstructionsand tooldescriptiondo forward. Soinstructionsis the highest-value lever for cross-tool guidance specifically for Claude.ai-targeted MCP servers. Structured fields remain in place for other clients (Cursor, Cline, MCP Inspector, custom integrations). - Connector error codes — Phase 1 primitives. New
Api/Services-Api/src/Model/ConnectorOperationError.ts— single class withcode: ConnectorErrorCode+details: {hint, helpUrl, retryAfterMs, debug}. Eight codes:UNAUTHENTICATED/UNAUTHORIZED/UNREACHABLE/RESOURCE_NOT_FOUND/UNSUPPORTED_CAPABILITY/RATE_LIMITED/INVALID_INPUT/EXTERNAL_API_ERROR.McpController.errorToJsonRpcmaps them to JSON-RPC with structureddata: {code, hint, helpUrl, retryAfterMs}carried alongside the human-readable message. Custom server-error codes for auth (-32001), unreachable (-32002), rate-limited (-32003), unsupported-capability (-32004); RESOURCE_NOT_FOUND/INVALID_INPUT land on -32602 so MCP UIs surface them as setup failures. Log severity is code-aware: client-correctable codes log atwarning, server-side codes aterror. No adapter has been migrated to throw these yet — Phase 2 (Hue pilot) is the next step. ExistingErrorthrows still flow through the unchanged INTERNAL_ERROR path. Design doc:~/.claude/plans/attlaz/connector-error-codes.md. - Hue colour round-trip:
colorXyinput onsetLightState. Direct fix for "the shape you read out isn't the shape you write back in" —listLights/getLightStatereturn colour as CIE 1931 xy chromaticity, butsetLightStatepreviously only accepted RGB/hex. NewcolorXy: {x, y}input takes the same shape, passed straight through to Hue (no lossy RGB→xy gamma+matrix conversion). Precedence:colorXy > colorHex > color(RGB). Schema descriptions on both sides updated to point at the round-trip — thecolorfield inHueLightSchemanow tells the agent it can be passed back viacolorXy. - Hue device-name resolution bug fix.
listLightsandgetLightStatewere returning stale product-default names ("Hue color lamp 1","Hue Smart plug 1") instead of the names the user sets in the Hue app. Root cause: Hue CLIP v2 splits each bulb into a device resource (carries the user-facing name inmetadata.name) and a light service (its ownmetadata.namedefaults to the product name and isn't updated on rename).HueClient.getLights()was readinglight.metadata.name. Fix: fetch/resource/lightand/resource/devicein parallel, build adeviceId → devicemap, resolve each light throughlight.owner.ridto its device, then usedevice.metadata.name. Falls back to the light's own name only if the device lookup misses (corrupted owner reference). Same join is the foundation for future room/zone support — Hue rooms reference device ids, not light ids.
Adapter operation catalogue (Services-Api + Library/Core)
New attlaz.adapter_operation table tracks every operation any adapter has ever exposed, across time: PLANNED (admin roadmap entry), ACTIVE (decorator present), BETA (decorator present with stability: 'beta'), DEPRECATED (auto-set when a decorator disappears), REMOVED (admin-only final state). Code-synced columns (status, description, effect, category) are rewritten on every sync; admin-editable columns (override_description, deprecation_note, replacement_op, external_docs_url) are preserved. The runtime registry (ConnectorOperationRegistry, served by GET /services/operations) remains the authoritative view of "what's callable in this process right now"; the DB ledger is the lifecycle view that survives across versions and includes planned/deprecated entries the runtime doesn't know about.
- New model + repository in Library/Core:
AdapterOperation(Attlaz-Connection/Model),AdapterOperationStatusenum (fromStringvalidates at the DB boundary instead of blind-casting),AdapterOperationRepositoryinterface +AdapterOperationSqlRepositoryimplementation. Domain methods on the entity (createFromCode,applyCodeSyncedFields,markDeprecated) — repository exposes genericinsert(op)/update(op)and field-protection lives on the model. - Schema migration:
Library/Core/database/adapter-operation-schema.sql. Seed file (adapter-operation-seed.sql) populates examplePLANNEDrows for the four existing connectors using deterministic SHA-derived 27-byte ids so reruns are idempotent. - Sync is explicit, not automatic. Run
node --env-file=.env dist/Catalog/syncOperationCatalog.script.jsfrom Services-Api after deploys. Deliberately not wired into boot — keeps staging stable, avoids replica races on first start, and prevents an aborted boot from leaving the catalogue inconsistent. - Dev CLI (
listOperationCatalog.script.ts) prints the catalogue grouped by adapter with PLANNED first — the dev punch list. Adapter.capabilities: string[]is now@deprecated— superseded by per-operation rows.AdapterSqlRepository.update()still writes the column for back-compat; a TODO marks the eventual removal.
Service → Connector rename across Services-Api
The runtime side of the iPaaS now uses "Connector" instead of "Service" — matches industry vocabulary (Workato, MuleSoft, Tray.io, Power Automate) and removes the overload with the GoF "adapter" pattern that "Adapter" carries elsewhere in software engineering. Platform-side terminology (DB tables adapter / adapter_operation, Library/Core entities Adapter / AdapterId / AdapterKey, Services-Api package name, /services/* route URLs) stays as "Adapter" for now — full migration is intended but unscheduled.
- Decorator:
@ServiceOperation(...positional)→@AdapterOperation(...)→@ConnectorOperation({adapterKey, operation, description, argumentsSchema, ...}). Now takes a single options object and exposes three new optional fields:stability: 'stable' | 'beta'(defaultstable) — controls whether the catalogue marks the op asACTIVEorBETA.effect: string | null— free-form; suggested vocabularyread/write/delete/invoke. Drives safety hints + future permission models ("this connection is read-only").category: string | null— free-form domain label (spreadsheet,device-control,ai-generation, …). Vocabulary will converge organically; intentionally not enumerated.
- 34 files renamed via
git mv(history preserved): registry, request/result, options, registration;ServiceManager→ConnectorManager;ServiceCommandController→ConnectorCommandController;ServiceCapabilitiesController→ConnectorCapabilitiesController;ServiceOAuth*→ConnectorOAuth*; 20 concrete*Serviceclasses →*Connector(GoogleSheetsConnector,HueConnector,OpenAIConnector,SshConnector, all the OAuth-only providers); theExternal*Serviceinterfaces →External*Connector. - Internal field renames:
MethodRegistration→ConnectorOperationRegistrationwithtarget/methodNamefields (wasserviceObject/serviceMethod);adapterKeytyped asAdapterKeyinternally (decorator option still accepts plainstringfor ergonomics, wrapped on register). - JSON wire changes:
GET /services/operationsresponse now usesconnectors/connectorkeys (wasservices/service)._objectmarker onConnectorOperationRequest.toJSON()is'ConnectorOperation'(was'ServiceOperation'). - Deferred for next round:
src/Service/directory name, theServices-Apipackage name itself, and the/services/*route URLs — all touch public surface (k8s, Gateway, build, external API clients).
MCP tools/list quality pass — adapter operations
Services-Api's MCP tools/list and tools/call now emit LLM-facing tool definitions. Driven by external review feedback (Claude Desktop's read of the Hue MCP at api.attlaz.com/mcp/<connectionId>): terse descriptions, missing parameter docs, no output schemas, no safety hints. Surface covered:
- Layer changes (
McpController.ts+Model/ConnectorOperation.ts): every tool definition now carriesoutputSchema(whenresponseSchemais declared),annotations(readOnlyHint/destructiveHint/idempotentHint/openWorldHint) derived fromeffect, and an optionaltitlefor display.tools/callresponses now emit MCP 2025-06-18structuredContentalongside text content for object-shaped returns.effecttightened fromstring | nullto'read' | 'write' | 'delete' | 'invoke' | nullstring union (all existing usages already fit — no breaking change).'invoke'and'write'collapse to identical annotations (non-destructive, non-read-only) — the distinction stays in our catalogue but doesn't leak to MCP clients. Default behaviour matters: MCP'sdestructiveHintdefaults totruewhen omitted, so non-destructive mutations must explicitly emitfalseto avoid being gated by careful clients. - Adapter sweep (Hue, Google Sheets, OpenAI, Ssh): every operation got a rewritten LLM-facing
descriptionwith when-to-use guidance vs alternatives, side effects, examples, and gotchas;.describe()on every Zod parameter with semantics, units, and edge cases (e.g. mirek with Kelvin equivalents, "brightness: 0 ≠ off", hex range and shorthand expansion);responseSchemaeverywhere it fits cleanly as an object;titleon operations whose camelCase name isn't self-explanatory.HueConnector.setLightStateeffectcorrected from'invoke'→'write'. - Hue setLightState extensions (from second-round reviewer feedback):
transitionMs(smooth fades, maps to Hue v2dynamics.duration),colorHex(CSS-style hex with 3- and 6-digit forms),colorTemperatureKelvin(intuitive scale converted to mirek internally with clamping to Hue's 153–500 range). Precedence when both formats are passed:colorHex>color(RGB),colorTemperatureKelvin>colorTemperature(mirek). - Hue listLights capability flags: response now reports
dimmable,supportsColor,supportsColorTemperatureper bulb — derived from presence of the corresponding service in the Hue v2 raw response, no extra API call. Lets agents check capability before sending colour to a white-only bulb (the bridge silently ignores it; previously no way to know). - Reachability deferred: Hue v2 doesn't expose
reachableon the light resource. It lives on/resource/zigbee_connectivitykeyed by device id, needs a multi-step join (light → owner device → zigbee_connectivity → status). Comment inHueClient.tsflags it. - Skipped
responseSchema:GoogleSheetsConnector.getSheetValue(returns rawany[][]) andOpenAIConnector.prompt(returns bare string). The official MCP SDK typesstructuredContentas object-only — wrapping these in{values: ...}/{text: ...}is a wire-format change deferred to a separate round. TODOs in code. - Cross-adapter error codes — design only: separate plan drafted at
~/.claude/plans/attlaz/connector-error-codes.md. Proposes an 8-code taxonomy (UNAUTHENTICATED, UNAUTHORIZED, UNREACHABLE, RESOURCE_NOT_FOUND, UNSUPPORTED_CAPABILITY, RATE_LIMITED, INVALID_INPUT, EXTERNAL_API_ERROR) with hints/helpUrls surfaced via JSON-RPCerror.datafor MCP. Not implemented.
MCP: connection validation + access check at initialize
The MCP endpoint silently accepted any connection id at initialize, then failed later at tools/list with a generic JSON-RPC -32603 internal error — which Claude's UI rendered as "no tools available" rather than a setup failure. Two real bugs fixed:
McpController.handleInitializenow callsConnectorManager.getAdapterConnection(id, userId)before setting up the session. Bogus connection ids fail immediately with JSON-RPC-32602 Invalid params: Unknown adapter connection: <id>. Cross-user attempts fail with-32602 Invalid params: No access to adapter connection: <id>.- User access check is now enforced.
ConnectorManager.getAdapterConnection(id, userId?)runsEntityAccessService.hasAccessToProject(userId, connection.projectId, true)whenever a user id is supplied; throwsNoAccessToEntityErrorif the user can't see the connection's project. The HTTP path (POST /services/:adapterConnectionId/:command) routes throughConnectorManager.handle()which passes the user fromAuthorizationDetailsautomatically, so the access check covers both surfaces. Previously the// TODO: validate that user has accesslived unattended ingetAdapterConnection. - Error type discipline:
Connection not foundthrowsEntityNotFoundError(not bareError) so callers can distinguish missing-row from no-access cleanly. Both are logged atwarning(expected client errors), noterror(server bugs).
OAuth client lookup fixed — sign-in restored across all flows
Login was failing with invalid_client (Web App password grant) and Unable to parse enum OAuthGrantType from string: Unknown value "password|refresh_token" (server logs). Root cause: the oauth_client.grant_types and oauth_client.redirect_uri columns had been migrated to a | separator, but OAuthClientSqlRepository was still using ,. Fixed via ScopeHelper.{serialiseNoNull,unserialiseNoNull} (Library/Core), matching the convention all other OAuth list columns already use (scopes, ip_allowlist).
- Heads-up for ops: any existing
oauth_clientrow written by the old code path may still have,separators. Check withSELECT id, grant_types, redirect_uri FROM platform.oauth_client WHERE grant_types LIKE '%,%' OR redirect_uri LIKE '%,%'andUPDATE … SET grant_types = REPLACE(grant_types, ',', '|').
OAuth popup security: explicit callback_uri + origin allowlist
The OAuth popup (/app/:adapterKey/connect) previously used the browser's Referer header to determine the postMessage target origin — fragile under any tightening of Referrer-Policy, and silently broken when the header was empty. Replaced with:
callback_uriquery param (the Web App already sends it). Server parses the origin and uses it aspostMessagetarget.web_app_originsallowlist (new Services-Api env var, defaults tohttps://app.attlaz.com,https://beta.app.attlaz.com). Rejects callback origins not on the list — closes the popup-phishing vector where a malicious page could request the OAuth flow with?callback_uri=https://attacker.comand receive auth tokens via postMessage.- Internal field rename:
OAuthState.refererUri→popupTargetOrigin(more accurate name now that it's no longer Referer-sourced).
OAuth-to-Services-Api migration completed in production
The /app/* route (OAuth flow initiation + callback for adapter connections) had been moved from Core-Api to Services-Api in code but never reached prod's platform.api_gateway_route table. INSERT for prefix='app' → Services-Api endpoint added; Core-Api's AuthAppController is now confirmed dead and removed in source. Also requires google_client_id / google_client_secret env vars on Services-Api (previously only set on Core-Api). GitHub OAuth app's authorized callback URL updated to drop the stale /alpha/ prefix.
MCP server info: title + description
Services-Api's MCP initialize response now includes title: "Attlaz" (MCP 2025-06-18 spec — display name distinct from technical name) and description: "Run flows and integrations from your Attlaz workspace". Claude.ai may render the description under the connector name; "Other tools" grouping is curated by Anthropic for non-featured integrations, so this won't change the section heading — separate process to apply for the featured connectors program.
HealthAlert updated_at aligned NOT NULL
DB column changed from NULL to NOT NULL, matching the HealthAlert.updated: Date (non-null) type that's always set in the constructor (this.updated = this.date). Parser updated correspondingly. Internal — no behaviour change for callers, just closes a latent type lie.
Date parsing + row-typing defensiveness sweep (Library/Core)
16 SQL repositories switched from as Date casts to DateHelper.parse(... as string | Date) / parseNullable(... as string | Date | null) — Redis-cache-roundtrip-safe (cache stringifies Dates; old code would silently return strings typed as Date and crash downstream). 10 SQL repository row parsers migrated from (row: any) to (row: RowRecord) with explicit field casts matching DDL column types. Internal — surfaces invalid DB data as a clear parse error instead of an undefined cascade.
Bug fix: duplicate event-channel notifications eliminated
When multiple subscribers to a flow's events shared a delivery channel (e.g. two users on the same Slack channel), the Event-Service was sending the channel notification twice — once per subscriber. Root cause: Utils.unique on a ChannelId[] did reference-equality deduplication, but ChannelId instances built from different DB rows (one per subscriber) are distinct refs even when they point to the same underlying KSUID. A new EntityId.dedupe static method (Library/Core) keys by toString() and correctly merges them. Two call sites fixed: FlowRunEventHelper.getChannelIds (Event-Service) and MarketPulseProductsController (Products-Api, where the bug was bloating the vendorRepository.getByIds() query payload — SQL deduped the result so it didn't surface visibly).
- New API:
EntityId.dedupe<T extends EntityId>(ids: T[]): T[]— use this anywhere you need to dedupe an array of entity IDs. PlainUtils.unique/new Set(...)will silently fail onEntityIdsubclasses; that pitfall now has a memory note for future work.
Per-tier storage capacity health alerts
Storage capacity health alerts are now emitted per tier instead of as a single combined alert. Four new HealthTestType values: storage_cache_usage_within_limit, storage_persistent_usage_within_limit, storage_vault_usage_within_limit, storage_infrastructure_usage_within_limit — the last adds an Infrastructure tier alongside the existing Cache / Persistent / Vault tiers documented in the storage reference.
- Library/Core + JS Client (
attlaz-client): enum values added in both repos. Required in lock-step becauseHealthTestType.fromStringthrows on unknown values — a single unrecognised alert would otherwise break the entire health-alert response parse in the Web App. - Web App: bumped
attlaz-client. The health alert list (health-alert-list.component.ts) now renders proper title / message / fix-button per tier ("Cache storage usage above limit", etc.) instead of falling through to a blank row. Each tier routes to Manage infrastructure on fix, matching queue/worker alert behaviour. The home-page projects-list widget rolls them into the per-environment "General" alert count — no per-type breakdown there, by design. - Internal refactor:
health-alert-list.component.tsswitched from three parallel switches keyed onHealthTestTypeto oneRecord<HealthTestType, AlertRenderer>map. FutureHealthTestTypeadditions are now exhaustiveness-checked at compile time instead of silently falling through to adefaultbranch.
2026-05-18
MCP OAuth Phase 6 — /oauth/authorize consent flow shipped to production
The browser-based authorization-code + PKCE flow that lets generic MCP clients (Claude Desktop's native MCP UI, claude.ai connectors, Cursor, custom) get a user-scoped access token without out-of-band staff provisioning. Phases 6.1–6.4 deployed.
- Gateway:
/oauth/authorizevalidates request (response_type, registeredredirect_uriper RFC 6749 §3.1.2.4, PKCE mandatory for public clients), stores params under a single-use 5-minute nonce, redirects browser to${app_url}/oauth/consent?nonce=…. - Web App (route
/oauth/consent?nonce=…): fetches consent details fromGET /oauth/consent-details, renders an "Allow<client_name>to access your Attlaz account?" screen with scope descriptions + redirect host, Approve/Deny buttons. - Confirm endpoint:
POST /oauth/authorize/confirmwith{nonce, mode: 'preflight'|'approve'|'deny'}. Approve issues an auth code viaOAuth.saveAuthorizationCodeand persists the approval tooauth_client_approval. Preflight auto-approves silently if an existing approval covers the requested scope set (includingresource:<url>audience entries) — skips the consent UI on subsequent reconnects. Deny redirects to client with?error=access_deniedper RFC 6749 §4.1.2.1. - Connected Apps management: new Web App tab at
/settings/user/connected-apps. EndpointsGET /oauth/connected-apps+DELETE /oauth/connected-apps/:clientId. Revoke removes the approval row so next reconnect re-prompts for consent. - Known limitation: revoking a Connected App does NOT yet kill active access/refresh tokens — they continue to work until expiry (4h access / 2w refresh per current config). Tracked as 🟠 in
TODO.md; OK for "tidy up old connections", not for "kill compromised client now". - New DDL:
oauth_client_approvaltable (composite PK(user_id, oauth_client_id)).
New Gateway env var: app_url
- Used to build the consent UI redirect URL. Same env var name Event-Service already uses for Web App deep-links. Defaults to
https://app.attlaz.com.
ResponseTimeAndLogHandle — Gateway-only, config-driven verbosity
- The per-request timing + MongoDB logging middleware is no longer wired in the base
ExpressServer. Backends (Core-Api, Services-Api, Products-Api, CDN-Api — anything usingBackendExpressServer) stop running it on startup. One less middleware on every internal proxied request. The middleware was already marked@deprecatedfor backend use; the code now matches the doc. FrontendExpressServer(Gateway-Api) overrides the newregisterRequestLogging()hook to wire the middleware. Two orthogonal verbosity flags now configurable instead of hardcodedfalse:request_log_verbose(env var, default false) — when on, captures request headers + body and response headers in the MongoDBApiRequestrecord. Useful for post-hoc inspection.request_log_console_every_request(env var, default false) — when on, emits an INFO log line for every request, not only slow/error ones. Useful for live-tailing during debugging (e.g. tracing an MCP client's discovery flow).
- Defaults are off → steady-state behaviour unchanged (slow/error to console, lean Mongo records). Toggle the env vars when investigating.
Bug fix: Gateway auth-required routes now actually require auth
/oauth/authorize/confirmand/oauth/connected-apps(GET + DELETE) were initially wired ininitPublicRoutes, which runs BEFORE theoauth2Server.authenticate()middleware. The controllers correctly readrequestHelper.hasAuthenticatedUser()and rejected with 401 — but the auth middleware never ran in the first place, so any Bearer header was ignored. Moved toinitPrivateRoutes(registered after the auth middleware). The same pitfall now has a memory note: Bearer-required Gateway routes viaaddControllerRouteMUST go ininitPrivateRoutes.
2026-05-17
MCP OAuth 2.1 conformance — deployed to production
The conformance work documented in the 2026-05-15 / 2026-05-16 entries is now live at https://api.attlaz.com. Smoke-verified: RFC 8414 AS metadata served from /.well-known/oauth-authorization-server, RFC 9728 PR metadata served from /.well-known/oauth-protected-resource/mcp, and 401s on /mcp/* carry the WWW-Authenticate: Bearer realm="mcp", resource_metadata="…" discovery header.
Dynamic Client Registration (RFC 7591) — POST /oauth/register is now live
- MCP clients (Claude Desktop, Cursor, custom) can self-register as public OAuth clients without admin intervention. Endpoint is unauthenticated per RFC 7591 default; abuse mitigated by IP-rate-limit firewall.
- Always issues
client_type='public'withtoken_endpoint_auth_method='none'. Grants whitelisted toauthorization_code + refresh_tokenonly — nopassword, noclient_credentialsfor self-registered clients. - Validates
redirect_uris(HTTPS,http://localhost, or custom URI schemes for native apps),client_name(max 128 chars), and optionallogo_uri/client_uri/tos_uri/policy_uri(must be HTTPS). /.well-known/oauth-authorization-servernow advertisesregistration_endpoint.
Breaking: public OAuth clients can no longer submit a client_secret
- Previously: a public client (
client_type='public') that mistakenly sent aclient_secretto/oauth/tokengot a warning log and the secret was ignored. Request still succeeded. - Now: such requests are rejected with HTTP 401
invalid_client. Public clients must omitclient_secretentirely. - Affects: anything authenticating as the Web App OAuth client (
vu6sWqT6dX55Iw91tab8UceEg4on4YOA) or any DCR-registered client. The Web App was updated in commita0e739b1to usesetPublicClient(client_id)instead ofsetClientCredentials(client_id, client_secret). Third-party callers using the Web App'sclient_idto authenticate must also drop the secret.
Breaking: /app/<adapter>/connect rejects authenticated requests without prepare_state (except mode=auth-provider)
- The legacy flow that opened OAuth popups with
?access_token=...in the URL is no longer accepted. Clients must callPOST /app/<adapter>/prepare-connectfirst (Bearer auth via header) to receive a short-livedstatecode, then open/app/<adapter>/connect?prepare_state=<code>. - Social-login flow (
mode=auth-provider) is unaffected — it's unauthenticated by design. - Affects: anything still using the pre-2026-05-17 Web App pattern of passing access tokens via query string for adapter OAuth popups.
OAuth model: OAuthGrantType + OAuthResponseType enums
- Internal refactor: the four supported grant types (
authorization_code,password,refresh_token,client_credentials) and the one supported response type (code) now live as enums in@attlaz/attlaz-core(OAuth/Model/OAuthGrantType.js,OAuth/Model/OAuthResponseType.js).OAuthClient.grantstyping tightened fromstring[]toOAuthGrantType[]. AS metadata and DCR endpoint now derive their advertised / accepted lists from the enum. - DB-facing change:
OAuthClientSqlRepository.rowToObjectnow strict-parses each grant viaOAuthGrantType.fromString— any row with a value outside the enum will throw on read. Pre-deploy SQL audit confirmed no dirty rows in production.
Bug fix: empty OAuth client scopes no longer break INSERT
OAuthClientSqlRepository.create()/update()were callingScopeHelper.serialise(client.scopes)which returnsnullfor empty arrays — butoauth_client.scopesisNOT NULL. Latent bug: existing seeded clients always had non-empty scopes, so the path was never hit. Surfaced when DCR became the first caller to create clients with empty scopes. Switched both methods toScopeHelper.serialiseNoNull(returns''for empty arrays). Library/Core1.191.1.
MCP reference page updated (MCP)
- New Authentication section documents audience binding (RFC 8707): pass
resource=https://api.attlaz.com/mcpon/oauth/tokento bind a token to the MCP surface. - Discovery subsection lists the two well-known endpoints and the
WWW-Authenticateflow that spec-compliant clients use to bootstrap. - Supported-adapter table updated: added Philips Hue (
listLights,getLightState,setLightState,listDevices); addedclearSheetto Google Sheets.
Runner pool edit dialog — UX refinements
- Per-field "Override" mat-slide-toggle replaces the implicit "edit value to override / clear to inherit" pattern. Toggle off = inherit (input disabled, shows resolved value); toggle on = pool override (always persists on save, even when the value matches the inherited one — pins the pool against future environment/zone changes).
- CPU and memory limits now use curated dropdowns (presets + "No limit" sentinel) instead of free-form integer input. When the resolved value is
null(no row anywhere in the inheritance chain), the dropdown auto-selects "No limit" to match the semantic meaning. - Dialog width pinned at 720px to stop toggle/hint-text changes from reflowing the modal.
JS Client: RunnerPool.parse robustness
- Defensive parsing of
configuration.*fields: missing or malformed{value, source}envelopes fall back to a defaultResolvedValueinstead of crashing the dialog. Each parse run emits a singleconsole.warnlisting any missing fields per pool, so server-side regressions stay visible. - Bug fix:
RunnerPool.configuration.imageIdwas never populated because the API auto-strips_idfrom snake_case keys (so the wire carriesimage, notimage_id). Parser corrected.
Per-flow runner pool assignment
- Flows can now be pinned to a specific runner pool within their environment. New "Runner Pool" autocomplete in the edit-flow dialog; empty selection falls back to the environment's default pool (the existing behavior).
Flow.runnerPoolIdis now read from the API response by the JS Client (raw.runner_pool→Flow.runnerPoolId: string | null) and accepted on flow create/update bodies byCore-Api(runner_poolfield onPOST /projects/:projectId/flows).
2026-05-16
MCP client integration — end-to-end verified
Generic MCP clients (Claude Desktop, Cursor, mcp-remote, etc.) can now connect to Attlaz adapter connections as tools. Full chain working: RFC 9728 discovery → RFC 8707 audience-bound tokens → strict audience enforcement → JSON-RPC tool execution.
RFC 8707 Resource Indicators — audience binding via scopes
- Pass
resource=<url>onPOST /oauth/tokento bind the issued token to a specific resource. The token's scope set will includeresource:<url>. - Gateway enforces audience strictly: a token carrying any
resource:<url>scope can only be used against URLs that prefix-match the resource. Used at any other URL → HTTP 403 with body"Token audience does not include this resource". - Tokens issued without
resource=are unbounded (today's existing behavior preserved). - Path comparison is boundary-aware:
resource:…/mcpcovers/mcpand/mcp/<id>but not/mcpfoo. - Audience enforcement is multi-resource-ready (the check accepts a match against any
resource:scope on the token). Initial token issuance binds to a single resource per request.
Protected Resource Metadata (RFC 9728)
- The MCP resource document is now at
/.well-known/oauth-protected-resource/mcp(path-scoped per RFC 9728 §3.1). - Tells generic MCP clients where the Authorization Server lives and what
resourcevalue to request.
Authorization Server Metadata (RFC 8414)
- AS metadata available at
/.well-known/oauth-authorization-serveradvertising the issuer URL, authorization/token endpoints, supported grants, PKCE methods (S256).
PKCE (RFC 7636) enforcement on authorization_code grant
- Authorization code grants now accept PKCE:
code_challenge+code_challenge_methodmay be passed at/oauth/authorizeand the matchingcode_verifierat/oauth/token. OnlyS256is accepted;plainis rejected per OAuth 2.1. - Public clients (
client_type='public') MUST use PKCE on theauthorization_codegrant. Requests withoutcode_challengeare rejected. - DDL:
oauth_authorization_codegained nullablecode_challenge VARCHAR(128)andcode_challenge_method ENUM('S256')columns.
WWW-Authenticate discovery header
- 401 responses on
/mcp/*now includeWWW-Authenticate: Bearer realm="mcp", resource_metadata="…/.well-known/oauth-protected-resource/mcp"so clients can discover the AS from a single failed request.
MCP protocol version negotiation
- The MCP
initializehandshake now negotiates the protocol version by echoing the client's requested version (instead of strict-rejecting anything except a hardcoded value). Confirmed compatible with mcp-remote requesting2025-11-25.
Breaking change (internal): _formatted flag no longer leaks
- The
_formatted: trueinternal serialiser flag (used by MCP/metadata controllers to bypass snake_case transformation) is now stripped before serialisation. Strict JSON-RPC clients that validate against unknown keys (e.g. mcp-remote's Zod validator) now accept our responses.
Fixed
insufficient_scopeerrors now return HTTP 403 (was 500). Library/ApiErrorHelpernow mapsOAuthErrorsubclasses through to their correct status codes.
Schema
scopescolumns onoauth_access_token,oauth_refresh_token,oauth_authorization_code,oauth_clientwidened fromVARCHAR(255)toTEXTto accommodate URL-prefixed scope values.
MCP client setup (documentation update coming): generic MCP clients require an absolute-URL-style resource indicator. For Claude Desktop on Windows, the most reliable launch path is the mcp-remote bridge via a globally-installed wrapper (path without spaces), passing the Bearer token via --header. Long-term, OAuth client self-registration (RFC 7591 / Dynamic Client Registration) will replace the manual token approach — not yet implemented.
2026-05-15
MCP OAuth 2.1 conformance (Phases 1–4)
- PKCE (RFC 7636) support added on
authorization_codegrant.code_challenge+code_challenge_methodcolumns added tooauth_authorization_code. OnlyS256accepted;plainrejected. Public OAuth clients must use PKCE. - AS metadata document published at
/.well-known/oauth-authorization-server(RFC 8414). - Protected Resource metadata for the MCP resource published at
/.well-known/oauth-protected-resource/mcp(RFC 9728 §3.1, path-scoped — root URL reserved for a future API-wide PR document). WWW-Authenticate: Bearer realm="mcp", resource_metadata="…"header now sent on 401 responses to/mcp/*.- RFC 8707 resource indicator (
resource=parameter on/oauth/token) now binds the issued token to that resource. Audience binding is enforced by Gateway middleware: if the token carries aresource:<url>scope, the request path must prefix-match. Tokens issued withoutresourceremain unbounded. - Standalone
mcpscope removed in favor of the genericresource:<url>audience model. - DDL:
scopescolumns onoauth_access_token,oauth_refresh_token,oauth_authorization_code,oauth_clientwidened fromVARCHAR(255)toTEXTto accommodate URL-prefixed scope values.
Fixed
insufficient_scopeerrors now return HTTP 403 (previously 500) — Library/Api error handler now mapsOAuthErrorsubclasses to their correct status code.
HealthAlert architecture overhaul
- New
severityfield on health alerts (INFO | WARNING | ERROR) — separated fromstatus(PASSED | FAILED | SKIPPED). Resolves prior issue where every FAILED alert rendered red regardless of actual severity. - Storage usage health check now per storage type:
STORAGE_CACHE_USAGE_WITHIN_LIMIT,STORAGE_PERSISTENT_USAGE_WITHIN_LIMIT,STORAGE_VAULT_USAGE_WITHIN_LIMIT,STORAGE_INFRASTRUCTURE_USAGE_WITHIN_LIMIT. Thresholds: PASSED<80%/ WARNING≥80%/ FAILED≥100%/ SKIPPED if limit unset. - Health alert uniqueness key simplified from
(scopeId, healthTestType, parentScopeId)to(scopeId, healthTestType). Pool-level alerts now visible to API. - New endpoint:
GET /health/by-parent/:parentScopeType/:parentScopeIdreturns all alerts for an environment in one call. CheckAdapterConnectionsCommandrewritten: Inactive connections now show asSKIPPEDwith reason; defensive parsing of validation responses isolates version-skew errors instead of flipping connections red; per-connection try/catch isolates failures.- Supervisor writes a heartbeat config row per environment after each check cycle; Maintenance gates alert expiration on heartbeat freshness (avoids stale-alert sledgehammer when Supervisor is down).
subtypecolumn dropped fromhealth_alert(was unused).
Worker / RunnerPool
Flow.parallelLimitdeprecated. Per-flow concurrency is now controlled at the RunnerPool layer viamaxRunners(each RunnerPool has its own queue, runners, and AMQPprefetch=1).RunnerPool.redeployendpoint:POST /runner-pools/:runnerPoolId/redeploytriggers a per-pool reconcile.- Runner pool edit dialog (Web App) now exposes all pool config fields: min/max runners, CPU/memory limits, image, runner/code endpoints, runner/code tokens.
Configuration
- Per-pool runner config returned via
getRunnerPoolConfiguration()with full inheritance chain (pool → environment → workspace → zone), each value carrying itssource. - Renamed config keys:
flow_run_worker_api_endpoint→flow_run_runner_endpoint,flow_run_worker_api_token→flow_run_runner_token,flow_run_code_api_endpoint→flow_run_code_endpoint,flow_run_code_api_token→flow_run_code_token. SQL migrationrename-runner-config-keys.sql. *_tokenconfig values now storeOAuthAccessTokenIdreferences rather than raw bearer tokens; bearer is resolved at reconcile time.InfrastructureConfigurationService.getValueWithSource()/getRunnerPoolConfiguration()return values together with their inheritance source.allowZoneFallbackparameter renamed toallowFallback;falsenow correctly means "supplied scope only" (was silently broken).
2026-05-14
Runner pool config response refactor
RunnerPoolsControllerGET response:effective_*flat fields replaced with a nestedconfigurationobject. Each field carries{ value, source }wheresourceis'pool' | 'environment' | 'workspace' | 'zone' | null.RunnersStatus.updated/QueueStatus.updatedrenamed to.datein the JS Client to match the API response.
2026-04-25
Google Sheets adapter — clearSheet operation
- New
clearSheetoperation on the Google Sheets adapter. Accepts a singlerangeparameter — a sheet name ("Sheet1") clears the whole sheet; A1 notation ("Sheet1!A1:B10") clears a specific range. Returns{ clearedRange }. Wraps the Google Sheetsspreadsheets.values.clearAPI.
2026-04-17
AppRunner reliability (Library/Core)
- Service processes now exit on
preRun()/run()rejection (previously zombied with broken state). - Event-driven readiness:
subscribeToRemoteServiceHealth()flips thereadyflag in response to remote-service disconnect/reconnect (Queue, MongoDB, MySQL/Postgres, Redis). Valkey still polled. - Graceful shutdown races a 20-second watchdog timer — pods can no longer zombie if a shutdown step hangs.
Command-Service performance
- Search index reindex: batch INSERTs (chunked 500), N+1 fixes on subscriber/member lookups, removed unnecessary queue round-trip in flow reindex handler.
- New
PromiseHelper.mapWithConcurrency()helper. Intra-run round-trips reduced 6 → 3 viaPromise.all; inter-run concurrency raised to 8. Streamed pagination (50k→500batch,~100MB→~1MBmemory). - Batch trigger fetch via
TriggerRepository.getByFlowsAndEnvironment; handler repository deduplication.
2026-04-01
MongoDB api-requests cleanup
api-requestsandapi-requests-IIcollections dropped to reclaim disk space;api-requestsrecreated with zstd compression and three optimized indexes.ApiRequestMongoDBRepository.setup()now retries collection creation on race conditions (up to 5 attempts).
2026-03
2026-03-30
Web App — zoneless + signals migration
- Angular app is now fully zoneless (Zone.js removed; no
ChangeDetectorRefcalls remain). StateServicemigrated from EventEmitters to signals.- Base component hierarchy deduplicated via
ReactiveOutletComponent. - Connection pages refactored to load once via
ROUTER_OUTLET_DATA. CollectionDataSourcegained retry logic, properdisconnect(), and error display.
Security
- Access tokens no longer logged to console (removed
console.logfromGoogleSheetsService). - Bearer tokens in query strings disabled (
allowBearerTokensInQueryString: false). - Token IP now captured from the actual request via
AsyncLocalStorage-basedOAuthRequestContext(previously hardcoded to0.0.0.0). - Slack webhook secret moved from source to
slack_webhook_pathenv var. - Firewall per-violation-type thresholds:
invalid password(3/2h block),access/refresh token(5/1h),unauthorized request(10/30m). Tokens masked in logs;Authorizationheader sanitized. - OAuth state/redirect URI validation:
OAuthStateStoreenforces 10-min TTL and single-use.
OAuth — public clients
oauth_clienttable gainedclient_type ENUM('confidential','public')column. Public clients can authenticate withoutclient_secret(PKCE used instead). Web App client switched to public.- Web App bundle no longer contains
client_secret. requireClientAuthentication: falsefor all grant types inOAuthServerFactory— enforcement moved toOAuth.getClient().
OAuth — state exchange for adapter connect
- New endpoint:
POST /app/:adapterKey/prepare-connect(Services-Api). Accepts Bearer token, returns a short-lived state code. GET /app/:adapterKey/connect?prepare_state=…accepts the state code as a query parameter; access token no longer needs to be passed in the OAuth popup URL.
Queue (message broker) fixes
- Async message handlers are now correctly awaited;
isHandlingMessageplaced in try/finally. - Race condition in
Queue.getChannel()/Queue.getOrCreateConnection()fixed via in-flight promise maps. ack()/nack()calls on closed channels now log a warning instead of throwing.
Performance
- Gateway response buffering removed — responses now stream directly to clients.
- Gateway body parser registered per-route on
/oauthand/system(512 KB); proxied requests stream raw to backends. Core-Api body limit raised to 2048 MB for storage uploads. - ~24
console.log/console.error/console.tablecalls across 10 services replaced withLoggercalls. - New
Logger.shouldProcess()short-circuit avoids creating log entries when no transport will accept the level. - StorageTransport queue drains in a single pass (previously O(n²) bouncing).
JS Client — dependency cleanup
- Removed
axiosandaxios-oauth-clientruntime dependencies.HttpClientnow uses nativefetch.OAuthClientgrant flows unified viarequestToken().
2026-03-28
Competitor Monitoring — product tags
- New table
pulse.catalog_product_tag(key/value classifications computed server-side). - Initial tags:
has_matches(yes/no),price_segment(5 equal-width quintiles). - Product list filtering:
?tag[price_segment]=low_price&tag[has_matches]=yesvia EXISTS subquery. - Filter-options endpoint returns
tags: { price_segment: [...], has_matches: [...] }alongside brands/categories/groups. - Price-segments widget reads counts from tags (single aggregate query) instead of loading all products.
- Web App filter bar: "Price segment" and "Has matches" surfaced as separate filters. Existing "Tags" label renamed to "Group" for user-created
ProductGroupentities.
Queue — delayed retry
- New
requeue_delayedresult type. Failing messages are republished to{queue}.delaywith configurable TTL (default 10s), then dead-lettered back to the main queue. Max retries: 50. Replaces the previous ineffectiverequeueAtBackOfQueue.
Infrastructure
- Rancher upgraded 2.12.0 → 2.13.3 on the local cluster (downstream agents auto-updated).
2026-03-25
Product matching — Fellegi-Sunter strategy refactor
- Match algorithm refactored to the Fellegi-Sunter probabilistic model with a strategy pattern (per-field scorers, m/u probability table, configurable per match job).
- New
NameTokenScorerfor product name matching. - Match-details endpoint exposes per-field scores and total confidence.
2026-03-20 – 2026-03-21
OAuth migration complete
- Services-Api is now the single OAuth handler for all adapter connections. New adapters live in one repository instead of three.
@ServiceOAuthclass decorator registers OAuth configuration (authorization URI, token URI, scopes, etc.) per adapter.- All six OAuth modes implemented in Services-Api:
authenticate-connection,authenticate-source-account,code-access,create-channel,auth-provider,create-auth-provider. - Provider-Token-Service now calls Services-Api over
DirectTransportfor token refresh.
2026-03-17
Products-Api — breaking change
VendorProductMatchRepository.getPriceIndex()now returnsMap<string, PriceIndexEntry>(wasMap<string, number>).PriceIndexEntryhas{ priceIndex, matchCount }. Callers must access.priceIndexfrom the map values.
2026-03-16
API — uniform filtering helpers
- Common filter parsing helpers added to
Library/Api/src/Helper/RequestHelper/BaseRequestHelper.ts:getDateRangeFilter(),getStringFilter(),getEnumFilter(). Query syntax:?param[gte]=…¶m[lte]=…for date ranges,?param=valuefor strings/enums. Documented atdocs/03-reference/01-essentials/02-api/filtering.mdx. Controllers migrate incrementally —MarketPulseProductsControlleris the first consumer.
2026-03-15 – 2026-03-19
PHP Client
ServiceEndpointURL corrected:/services/{id}/test→/services/{id}/{command}(matches actual API route).- New
getCapabilities(connectionId)method onServiceEndpoint. - New
ProviderTokenEndpoint:getProviderToken(),getProviderTokenAccessToken(),revokeProviderToken(). NewModel/ProviderToken/ProviderToken.phpandProviderTokenAccessToken.php.
Adapters
- Philips Hue adapter added:
listLights,getLightState,setLightState,listDevices. CLIP API v2; RGB→CIE xy conversion. - SSH adapter added:
listFiles,getFileInfo,readFile,tailFile,searchFile,listDirectories. 5-min idle connection pool. Shell args single-quote-escaped.
MCP provider
- Attlaz now exposes adapter connections as MCP tools via Streamable HTTP at
POST /mcp/:connectionId(JSON-RPC 2.0:initialize,tools/list,tools/call). Any@ServiceOperationbecomes an MCP tool automatically.
Infrastructure
- All 26 AppRunner-based services now expose Kubernetes health probes (
/healthz,/readyz,/startupz). - Gateway-Api: reactive backend status — proxy connection errors mark routes offline immediately; offline routes re-checked on demand with 5-second cooldown.
2026-03-10
JS Client
FlowEndpoint.getFlowSummaries()now acceptsCursorPagination. Previously fetched without pagination — projects with more than 10 flows silently dropped the overflow (API defaults tolimit=10).DirectTransportis now browser-compatible (nonode:cryptodependency). Constructor takes pre-computedsessionPayloadandsessionSignaturestrings; newstatic createSessionPayload()helper for Node callers.LoadAllHelperreplaced per-elementpushwithpush(...records)spread; added 50k guard against call-stack overflow on unexpectedly large datasets.
2026-03-11 – 2026-03-14
Competitor Monitoring — Web App integration
- Full Web App module added: catalog, products, competitor matches, statistics.
- API endpoints completed for product listing, match management, price-index calculation.
- Multiple N+1 query fixes (connections endpoint, match details).
- Cursor pagination audited and fixed across
FlowSummarySqlRepository,WorkspaceSqlRepository,SubscriberSqlRepository,PlatformApiRepository.
2026-03-03
ProviderToken endpoints
GET /provider-tokens/:id— status + display details (no access token).GET /provider-tokens/:id/access-token— raw access token, with expiry/revoke guards and audit logging.DELETE /provider-tokens/:id— soft revoke (setsstate=Inactive, stampsrevoked_at).
2026-02-28 – 2026-03-04
Security
- PKCE frontend implementation prepared for Web App (awaiting API support, shipped 2026-03-30).
- Gateway forwards
x-forwarded-host/x-forwarded-protobased ontrusted_proxy_ipsallowlist (avoids spoofing viax-forwarded-for).
JS Client
DirectTransportintroduced for internal service-to-service calls (HMAC-signed via sharedapi_secret). NewITransportinterface lets callers inject any transport viaClient.withTransport(transport, config?).DirectTransportsupports per-prefix routing to different base URLs (sorted longest-prefix-first).- Security: non-Bearer token types now throw
ClientErrorinstead of leaking the token in the query string. - Performance: HTTP client now uses a singleton instance to enable TCP keep-alive / connection reuse.
- Fixes:
'healt-alert'→'health-alert'cache key typo; renamedoathClientToken→oauthClientTokenthroughout; removed double expiry check insignRequest.
2026-02-14
- A project can now have multiple queues, allowing for separation of flow runs.
2025
2025-12-09
- Added PHP 8.5 platform.
- Added Node.js 24 and Node.js 25 platforms.
2025-11-29
Infrastructure upgrade
- Added 8 GB of swap memory to the service node.
- Upgraded MariaDB to 12.1.2 (from 11.7.2).
2025-11-12
- Gateway API now detects whether a backend route is online or offline and refuses to route to an offline route.
- Gateway API: route versioning via the
Attlaz-API-Versionheader. - Notion OAuth implementation added.
2025-11-11
- Fixed: Gateway routed to the wrong API version when requesting
beta. - Fixed: adapter icons in the Web App (beta only).
- More information about adapters now shown in the Web App.
- Web App now uses Tailwind CSS.
- Gateway and Core APIs now run on Node 25.
- Gateway API performance improved (better route rewriting).
2025-08-12
Infrastructure
- Rancher upgraded to 2.12.0.
- Cluster
local: Kubernetes v1.32.5+k3s1 → v1.33.2+k3s1. - Cluster
attlaz-infrastructure: Kubernetes v1.32.5+rke2r1 → v1.33.2+rke2r1.
2025-06-09
- Storage items are now stored internally as buffers — they take up less space and serialisation is optimised.
- Introduced the Content Delivery API: images can now be served publicly via the Attlaz CDN. Used by Market Pulse to serve catalog images so scraped data is stored once and no longer depends on vendor URLs staying stable.
2025-06-06
- Base PHP adapter: replaced Laminas DB with Doctrine DBAL. Laminas DB is no longer actively maintained; switching to Doctrine DBAL lets projects adopt PHP 8.4 and above. Currently in beta.
2025-05-29
- Flow indexation now uses a debounce mechanism. Instead of reindexing on every event (e.g. a flow run update), changes are batched over a 15-second window and applied once. Significantly reduces CPU and database load during event bursts while still keeping the index current.
- Web App upgraded to Angular 20.
- Web App log streams are now viewable directly in the Web App.
2025-05-19
- Log streams can now be assigned to any entity, not only projects.
- Flow log stream IDs are stored in the database.
- Deployment log stream IDs are stored in the database (not yet used).
- The API now returns a response for a flow run summary even if the summary doesn't exist yet.
- Started using typed API responses to guarantee response shape stability when the underlying code changes.
2025-05-08
- Moved infrastructure services off the Rancher 1 environment.
- Services now read configuration from the API (zone configuration).
- Queues are now truly durable. Previously the deployment hostname wasn't set, so it changed on every restart — meaning the RabbitMQ node name (used for the storage path) changed too, breaking durability.
- Deploy archives are now generated as
.tar.gz. - Large files are now stored using MongoDB GridFS.
- Storage now uses storage buckets, stored in the database as references for the collections. Allows per-bucket configuration (name, lifetime).
- Deploy archives are now stored in workspace storage. Each deploy tracks its storage bucket and storage key.
- It is now possible to define the active image on a platform, giving more control over which image is selected (e.g. for trying out new images before going live).
2025-01-17 — Release: Rigel
App 2.7.0
- Easier switching between workspaces and projects.
- Upgraded to Angular 19.
- Improved "edit flow" modal.
- Applications now shown in navigation.
- Table views upgraded to a new table view (improved item rendering and pagination).
- Navigate to project and workspace management pages via the select dialog.
- Search now covers more entities (indexed search).
Infrastructure
- RabbitMQ upgraded; lazy (persistent) queues now in use.
- Cleaned up old IDs.
2024
2024-10-28
(Entry recorded but no content captured.)
2024-08-11
Magento2 1.7.0
- Creates a
stock itemwhen one is not available forstock statusupon loading the stock.
2024-08-09
Worker 1.12.0
- Runs code based on the selected "run code strategy".
- Improved logging.
2024-08
- Push notifications are now shared across devices. Instead of creating a channel per device, push notifications go to a general channel and route to the devices you have selected.
2024-08-02
- Worker 1.11.13: improved internal logging; upgraded to Node 22.5.
- API Beta: fixed an issue creating new users where
new_idwas not set.
2024-07-30
Command processor 1.3.0
- Implemented partial indexation of flows to improve performance. Only the latest run status is updated on each change; a full indexation runs every 15 minutes.
2024-07-26
Magento2 Adapter v1.5.0
- New command/tool to generate catalog URL rewrites for entities that are missing them. Automatically detects which catalog items lack URL rewrites and generates them.
- Improved fetching of URL rewrites — filter by type and store view.
- New utility to quickly convert from id to SKU and from SKU to id.
- New repository to fetch aggregated sales numbers.
- Refactored CLI and
bin/magentotools. - It is now possible to use custom product types.
- General refactoring, code improvements, and removal of legacy code.
- Tested with Magento up to 2.4.7-p1.
2024-07-07
App 2.5.0
- Moved to standalone components (performance improvements).
- Locale selection (date format, time format, number format).
- Number formatting now follows the user's locale configuration.
2024-04-08
- Status page check improved to validate the scheduling service.
2024-03-12
- It is now possible to manage API keys through the app (create, update, revoke).
2024-03-08
- It is now possible to start code source deploys when calling a webhook.
2024-03-07
Security incident — internal test client credentials
A client ID and client secret were leaked from our source code. We removed the leaked information from our Subversion history and from deployed packages, and updated our pipeline to detect secrets before commit.
The compromised client has been disabled, and client activity has been reviewed — no signs of abuse. All tokens and access have nonetheless been revoked. As this was an internal test client, no customer data was ever accessible through these credentials.
2023
2023-12
- Deploy artefacts are now stored in a global storage. Workers fetch the required deploy package on spin-up before waiting for flow run requests.
2023 (date unrecorded)
- Each flow run can now use an individual code source.